@sema-agent/server 1.323.0 → 2.0.1

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 (565) hide show
  1. package/dist/approval-hmac.d.ts +40 -0
  2. package/dist/approval-hmac.js +67 -0
  3. package/dist/approval.d.ts +31 -0
  4. package/dist/approval.js +72 -4
  5. package/dist/audit.d.ts +26 -0
  6. package/dist/audit.js +40 -1
  7. package/dist/auth-bridge.d.ts +10 -0
  8. package/dist/auth-bridge.js +47 -4
  9. package/dist/auth-keys.d.ts +19 -0
  10. package/dist/auth-keys.js +13 -0
  11. package/dist/bake-runner/main.d.ts +5 -0
  12. package/dist/bake-runner/main.js +43 -4
  13. package/dist/bake-runner/protocol.d.ts +57 -0
  14. package/dist/bake-runner/protocol.js +58 -3
  15. package/dist/bake-runner/runner.d.ts +54 -0
  16. package/dist/bake-runner/runner.js +87 -5
  17. package/dist/bench/l8/artifact.d.ts +63 -0
  18. package/dist/bench/l8/artifact.js +61 -2
  19. package/dist/bench/l8/escape.d.ts +36 -0
  20. package/dist/bench/l8/escape.js +25 -0
  21. package/dist/bench/l8/index.d.ts +14 -0
  22. package/dist/bench/l8/index.js +14 -0
  23. package/dist/bench/l8/probes.d.ts +74 -0
  24. package/dist/bench/l8/probes.js +60 -0
  25. package/dist/bench/l8/run-probes.d.ts +43 -0
  26. package/dist/bench/l8/run-probes.js +45 -2
  27. package/dist/bench/s1/arms.d.ts +126 -0
  28. package/dist/bench/s1/arms.js +188 -13
  29. package/dist/bench/s1/live-deps.d.ts +66 -0
  30. package/dist/bench/s1/live-deps.js +249 -11
  31. package/dist/bench/s1/oracle.d.ts +49 -0
  32. package/dist/bench/s1/oracle.js +36 -1
  33. package/dist/bench/s1/repair-oracle-adapter.d.ts +33 -0
  34. package/dist/bench/s1/repair-oracle-adapter.js +30 -0
  35. package/dist/bench/s1/reviewer.d.ts +38 -0
  36. package/dist/bench/s1/reviewer.js +34 -0
  37. package/dist/bench/s1/row.d.ts +77 -0
  38. package/dist/bench/s1/row.js +62 -0
  39. package/dist/bench/s1/run-firm.d.ts +47 -0
  40. package/dist/bench/s1/run-firm.js +70 -4
  41. package/dist/bench/s1/runner-ctx.d.ts +71 -0
  42. package/dist/bench/s1/runner-ctx.js +36 -0
  43. package/dist/bench/s1/tasks.d.ts +108 -0
  44. package/dist/bench/s1/tasks.js +115 -0
  45. package/dist/boot/budget-tracing.d.ts +48 -0
  46. package/dist/boot/budget-tracing.js +86 -0
  47. package/dist/boot/config-center.d.ts +62 -0
  48. package/dist/boot/config-center.js +995 -0
  49. package/dist/boot/coordinators.d.ts +33 -0
  50. package/dist/boot/coordinators.js +97 -0
  51. package/dist/boot/execution-env.d.ts +26 -0
  52. package/dist/boot/execution-env.js +370 -0
  53. package/dist/boot/leader.d.ts +27 -0
  54. package/dist/boot/leader.js +81 -0
  55. package/dist/boot/reapers.d.ts +53 -0
  56. package/dist/boot/reapers.js +252 -0
  57. package/dist/boot/resolve-spec.d.ts +70 -0
  58. package/dist/boot/resolve-spec.js +1072 -0
  59. package/dist/boot/runner-deps.d.ts +101 -0
  60. package/dist/boot/runner-deps.js +343 -0
  61. package/dist/boot/runtime-caps.d.ts +21 -0
  62. package/dist/boot/runtime-caps.js +62 -0
  63. package/dist/boot/session-faces.d.ts +57 -0
  64. package/dist/boot/session-faces.js +157 -0
  65. package/dist/boot/shutdown.d.ts +50 -0
  66. package/dist/boot/shutdown.js +129 -0
  67. package/dist/boot/stores.d.ts +32 -0
  68. package/dist/boot/stores.js +361 -0
  69. package/dist/boot/workflow-orchestration.d.ts +46 -0
  70. package/dist/boot/workflow-orchestration.js +150 -0
  71. package/dist/boot-reclaim.d.ts +19 -0
  72. package/dist/boot-reclaim.js +20 -0
  73. package/dist/brain.d.ts +25 -0
  74. package/dist/brain.js +61 -1
  75. package/dist/budget.d.ts +66 -0
  76. package/dist/budget.js +128 -4
  77. package/dist/capabilities/builtin-tools.d.ts +3 -0
  78. package/dist/capabilities/builtin-tools.js +3 -0
  79. package/dist/capabilities/center-plugins.d.ts +17 -1
  80. package/dist/capabilities/center-plugins.js +45 -4
  81. package/dist/capabilities/center-prompts.d.ts +46 -0
  82. package/dist/capabilities/center-prompts.js +57 -3
  83. package/dist/capabilities/code-review-council.d.ts +28 -0
  84. package/dist/capabilities/code-review-council.js +40 -7
  85. package/dist/capabilities/collab-wire.d.ts +2 -0
  86. package/dist/capabilities/collab-workflows.d.ts +7 -0
  87. package/dist/capabilities/collab-workflows.js +44 -1
  88. package/dist/capabilities/oa-tools.d.ts +6 -0
  89. package/dist/capabilities/oa-tools.js +10 -0
  90. package/dist/capabilities/prompt.d.ts +14 -0
  91. package/dist/capabilities/prompt.js +14 -0
  92. package/dist/capabilities/prompts/code-review.d.ts +14 -0
  93. package/dist/capabilities/prompts/code-review.js +14 -0
  94. package/dist/capabilities/prompts/identity.d.ts +6 -0
  95. package/dist/capabilities/prompts/identity.js +6 -0
  96. package/dist/capabilities/prompts/team.d.ts +4 -0
  97. package/dist/capabilities/prompts/team.js +4 -0
  98. package/dist/capabilities/repo-tools.d.ts +11 -0
  99. package/dist/capabilities/repo-tools.js +8 -0
  100. package/dist/capabilities/sandbox-file-send.d.ts +95 -0
  101. package/dist/capabilities/sandbox-file-send.js +91 -3
  102. package/dist/capabilities/scenarios.d.ts +130 -0
  103. package/dist/capabilities/scenarios.js +180 -7
  104. package/dist/capabilities/select-environment-tool.d.ts +7 -0
  105. package/dist/capabilities/select-environment-tool.js +30 -0
  106. package/dist/capabilities/send-user-file-tool.d.ts +33 -0
  107. package/dist/capabilities/send-user-file-tool.js +65 -1
  108. package/dist/capabilities/skills.d.ts +29 -0
  109. package/dist/capabilities/skills.js +13 -2
  110. package/dist/capabilities/team.d.ts +22 -0
  111. package/dist/capabilities/team.js +22 -2
  112. package/dist/capabilities/tool-defer.d.ts +26 -0
  113. package/dist/capabilities/tool-defer.js +3 -0
  114. package/dist/config-center/apply-effective.d.ts +65 -0
  115. package/dist/config-center/apply-effective.js +204 -12
  116. package/dist/config-center/http-client.d.ts +34 -0
  117. package/dist/config-center/http-client.js +68 -0
  118. package/dist/config-center/restart-signal.d.ts +22 -0
  119. package/dist/config-center/restart-signal.js +39 -0
  120. package/dist/config-center/skills-mcp.d.ts +20 -1
  121. package/dist/config-center/skills-mcp.js +42 -7
  122. package/dist/config-center/types.d.ts +92 -1
  123. package/dist/config-lkg.d.ts +12 -0
  124. package/dist/config-lkg.js +41 -0
  125. package/dist/config-provider.d.ts +78 -0
  126. package/dist/config-provider.js +165 -2
  127. package/dist/config-types.d.ts +585 -1
  128. package/dist/config.d.ts +78 -2
  129. package/dist/config.js +874 -301
  130. package/dist/degenerate-instrument.d.ts +38 -0
  131. package/dist/degenerate-instrument.js +55 -3
  132. package/dist/elicitation.d.ts +39 -0
  133. package/dist/elicitation.js +87 -3
  134. package/dist/env-facts.d.ts +61 -0
  135. package/dist/env-facts.js +73 -7
  136. package/dist/fleet/fleet-bus.d.ts +185 -0
  137. package/dist/fleet/fleet-bus.js +333 -27
  138. package/dist/fleet/subagent-tail-bus.d.ts +11 -0
  139. package/dist/fleet/subagent-tail-bus.js +33 -2
  140. package/dist/fleet-client.d.ts +38 -0
  141. package/dist/fleet-client.js +44 -9
  142. package/dist/fleet-lease.d.ts +46 -0
  143. package/dist/fleet-lease.js +73 -5
  144. package/dist/hooks/branch-transcript.d.ts +8 -0
  145. package/dist/hooks/branch-transcript.js +33 -0
  146. package/dist/hooks/cc-stop-prompt.d.ts +42 -0
  147. package/dist/hooks/cc-stop-prompt.js +43 -1
  148. package/dist/hooks/hook-llm.d.ts +15 -0
  149. package/dist/hooks/hook-llm.js +47 -0
  150. package/dist/hooks/hook-runner.d.ts +89 -0
  151. package/dist/hooks/hook-runner.js +327 -18
  152. package/dist/http/idempotency.d.ts +31 -0
  153. package/dist/http/idempotency.js +34 -0
  154. package/dist/http/principal-gate.d.ts +17 -0
  155. package/dist/http/principal-gate.js +35 -3
  156. package/dist/http/route-ctx.d.ts +36 -0
  157. package/dist/http/routes/approvals-assistant.d.ts +25 -0
  158. package/dist/http/routes/approvals-assistant.js +253 -23
  159. package/dist/http/routes/attachments.js +22 -4
  160. package/dist/http/routes/capabilities.d.ts +7 -0
  161. package/dist/http/routes/capabilities.js +202 -1
  162. package/dist/http/routes/fleet.d.ts +6 -0
  163. package/dist/http/routes/fleet.js +94 -5
  164. package/dist/http/routes/images.d.ts +17 -0
  165. package/dist/http/routes/images.js +222 -14
  166. package/dist/http/routes/leader.d.ts +7 -0
  167. package/dist/http/routes/leader.js +10 -0
  168. package/dist/http/routes/memory-policy.d.ts +6 -0
  169. package/dist/http/routes/memory-policy.js +38 -6
  170. package/dist/http/routes/notify-wake.d.ts +10 -0
  171. package/dist/http/routes/notify-wake.js +19 -3
  172. package/dist/http/routes/observability.d.ts +6 -0
  173. package/dist/http/routes/observability.js +19 -2
  174. package/dist/http/routes/runs.d.ts +15 -0
  175. package/dist/http/routes/runs.js +464 -41
  176. package/dist/http/routes/session-sync.d.ts +15 -0
  177. package/dist/http/routes/session-sync.js +229 -19
  178. package/dist/http/routes/sessions-list.d.ts +6 -0
  179. package/dist/http/routes/sessions-list.js +35 -4
  180. package/dist/http/routes/sessions.d.ts +14 -0
  181. package/dist/http/routes/sessions.js +176 -31
  182. package/dist/http/routes/side-query.d.ts +8 -0
  183. package/dist/http/routes/side-query.js +26 -1
  184. package/dist/http/routes/tasks.d.ts +18 -0
  185. package/dist/http/routes/tasks.js +501 -38
  186. package/dist/http/routes/trace-usage.d.ts +10 -0
  187. package/dist/http/routes/trace-usage.js +66 -14
  188. package/dist/http/routes/workflows.d.ts +12 -0
  189. package/dist/http/routes/workflows.js +180 -19
  190. package/dist/http/run-meta.d.ts +11 -0
  191. package/dist/http/run-meta.js +6 -0
  192. package/dist/http/send.d.ts +19 -0
  193. package/dist/http/send.js +11 -0
  194. package/dist/http/server.d.ts +255 -0
  195. package/dist/http/server.js +921 -75
  196. package/dist/http/sse-log.d.ts +15 -0
  197. package/dist/http/sse-log.js +16 -0
  198. package/dist/http/tar.d.ts +9 -0
  199. package/dist/http/tar.js +21 -5
  200. package/dist/http/wire-gate.d.ts +9 -0
  201. package/dist/http/wire-gate.js +9 -0
  202. package/dist/http/wire-types.d.ts +187 -0
  203. package/dist/http/workspace-content.d.ts +8 -0
  204. package/dist/http/workspace-content.js +10 -0
  205. package/dist/images/bake-validate.d.ts +63 -0
  206. package/dist/images/bake-validate.js +71 -1
  207. package/dist/images/manifest.d.ts +19 -0
  208. package/dist/images/manifest.js +6 -1
  209. package/dist/index.d.ts +1 -5
  210. package/dist/index.js +10 -5
  211. package/dist/key-resolver.d.ts +23 -0
  212. package/dist/key-resolver.js +27 -2
  213. package/dist/leader/diffout.d.ts +23 -0
  214. package/dist/leader/diffout.js +8 -1
  215. package/dist/leader/diffup.d.ts +32 -0
  216. package/dist/leader/diffup.js +43 -0
  217. package/dist/leader/endpoint.d.ts +20 -0
  218. package/dist/leader/endpoint.js +25 -2
  219. package/dist/leader/fanout.d.ts +60 -0
  220. package/dist/leader/fanout.js +29 -3
  221. package/dist/leader/grader-env-factory.d.ts +74 -0
  222. package/dist/leader/grader-env-factory.js +62 -3
  223. package/dist/leader/leader.d.ts +97 -0
  224. package/dist/leader/leader.js +151 -4
  225. package/dist/leader/merge.d.ts +94 -0
  226. package/dist/leader/merge.js +112 -9
  227. package/dist/leader/planner.d.ts +52 -0
  228. package/dist/leader/planner.js +47 -3
  229. package/dist/leader/repair-oracle.d.ts +61 -0
  230. package/dist/leader/repair-oracle.js +59 -1
  231. package/dist/leader/repair-wire.d.ts +78 -0
  232. package/dist/leader/repair-wire.js +79 -2
  233. package/dist/leader/wire.d.ts +79 -0
  234. package/dist/leader/wire.js +255 -8
  235. package/dist/lsp/e2b-bridge.d.ts +16 -0
  236. package/dist/lsp/e2b-bridge.js +32 -4
  237. package/dist/lsp/e2b-manager.d.ts +10 -0
  238. package/dist/lsp/e2b-manager.js +18 -2
  239. package/dist/lsp/lsp-frames.d.ts +12 -0
  240. package/dist/lsp/lsp-frames.js +12 -0
  241. package/dist/lsp/manager.d.ts +12 -0
  242. package/dist/lsp/manager.js +35 -2
  243. package/dist/lsp/types.d.ts +10 -0
  244. package/dist/lsp/ws-transport.d.ts +12 -0
  245. package/dist/lsp/ws-transport.js +53 -5
  246. package/dist/lsp-evict.d.ts +14 -0
  247. package/dist/lsp-evict.js +15 -1
  248. package/dist/main.js +530 -2129
  249. package/dist/memory-export.d.ts +1 -0
  250. package/dist/memory-export.js +4 -0
  251. package/dist/memory-scope.d.ts +49 -0
  252. package/dist/memory-scope.js +87 -4
  253. package/dist/memory-sync-client.d.ts +24 -0
  254. package/dist/memory-sync-client.js +41 -2
  255. package/dist/memory-sync.d.ts +23 -0
  256. package/dist/memory-sync.js +70 -1
  257. package/dist/model-select.d.ts +39 -0
  258. package/dist/model-select.js +42 -1
  259. package/dist/observability/cost-quota.d.ts +20 -0
  260. package/dist/observability/cost-quota.js +4 -0
  261. package/dist/observability/cost-taxonomy.d.ts +45 -0
  262. package/dist/observability/cost-taxonomy.js +34 -0
  263. package/dist/observability/logger.d.ts +1 -0
  264. package/dist/observability/logger.js +6 -0
  265. package/dist/observability/metrics.d.ts +13 -0
  266. package/dist/observability/metrics.js +63 -0
  267. package/dist/observability/otel-exporter.d.ts +9 -0
  268. package/dist/observability/otel-exporter.js +11 -3
  269. package/dist/observability/principal-context.d.ts +9 -0
  270. package/dist/observability/principal-context.js +9 -0
  271. package/dist/observability/prompt-manifest.d.ts +37 -0
  272. package/dist/observability/prompt-manifest.js +35 -1
  273. package/dist/observability/rate-limit.d.ts +10 -0
  274. package/dist/observability/rate-limit.js +1 -0
  275. package/dist/observability/tool-trace.d.ts +24 -0
  276. package/dist/observability/tool-trace.js +41 -1
  277. package/dist/orchestration/hardened-vm-runner.d.ts +107 -0
  278. package/dist/orchestration/hardened-vm-runner.js +119 -4
  279. package/dist/orchestration/hardened-vm-worker-runner.d.ts +14 -0
  280. package/dist/orchestration/hardened-vm-worker-runner.js +26 -1
  281. package/dist/orchestration/hardened-vm-worker.js +27 -0
  282. package/dist/orchestration/subagent-steer.d.ts +44 -0
  283. package/dist/orchestration/subagent-steer.js +45 -1
  284. package/dist/orchestration/workflow-agent-steer.d.ts +54 -0
  285. package/dist/orchestration/workflow-agent-steer.js +80 -1
  286. package/dist/orchestration/workflow-completion-inbox.d.ts +189 -2
  287. package/dist/orchestration/workflow-completion-inbox.js +283 -32
  288. package/dist/orchestration/workflow-notify-journal.d.ts +132 -1
  289. package/dist/orchestration/workflow-notify-journal.js +187 -13
  290. package/dist/parked-decide.d.ts +61 -0
  291. package/dist/parked-decide.js +58 -1
  292. package/dist/per-task-image.d.ts +54 -0
  293. package/dist/per-task-image.js +41 -0
  294. package/dist/plan-cache-probe.d.ts +35 -0
  295. package/dist/plan-cache-probe.js +18 -3
  296. package/dist/plugins/approval-exemption-store.d.ts +7 -0
  297. package/dist/plugins/approval-exemption-store.js +28 -2
  298. package/dist/plugins/approval-store-sql.d.ts +64 -0
  299. package/dist/plugins/approval-store-sql.js +32 -0
  300. package/dist/plugins/background-agent-store-sql.d.ts +55 -0
  301. package/dist/plugins/background-agent-store-sql.js +91 -4
  302. package/dist/plugins/background-shell-support.d.ts +98 -0
  303. package/dist/plugins/background-shell-support.js +117 -13
  304. package/dist/plugins/blob-backend.d.ts +104 -0
  305. package/dist/plugins/blob-backend.js +163 -6
  306. package/dist/plugins/breaker-state-sql.d.ts +65 -1
  307. package/dist/plugins/breaker-state-sql.js +46 -8
  308. package/dist/plugins/caching-session-store.d.ts +42 -0
  309. package/dist/plugins/caching-session-store.js +63 -3
  310. package/dist/plugins/checkpoint-store-sql.d.ts +179 -0
  311. package/dist/plugins/checkpoint-store-sql.js +239 -11
  312. package/dist/plugins/file-outcome-sink.d.ts +4 -0
  313. package/dist/plugins/file-outcome-sink.js +9 -0
  314. package/dist/plugins/file-resume-anchor-store.d.ts +20 -0
  315. package/dist/plugins/file-resume-anchor-store.js +43 -4
  316. package/dist/plugins/file-run-store.d.ts +90 -0
  317. package/dist/plugins/file-run-store.js +145 -13
  318. package/dist/plugins/file-snapshot-store-sql.d.ts +133 -1
  319. package/dist/plugins/file-snapshot-store-sql.js +178 -10
  320. package/dist/plugins/file-workflow-journal-store.d.ts +10 -0
  321. package/dist/plugins/file-workflow-journal-store.js +10 -0
  322. package/dist/plugins/fork-routing-session-store.d.ts +47 -0
  323. package/dist/plugins/fork-routing-session-store.js +51 -3
  324. package/dist/plugins/host-platform.d.ts +69 -0
  325. package/dist/plugins/host-platform.js +81 -2
  326. package/dist/plugins/image-bake-store-sql.d.ts +136 -1
  327. package/dist/plugins/image-bake-store-sql.js +241 -7
  328. package/dist/plugins/image-index-sql.d.ts +46 -0
  329. package/dist/plugins/image-index-sql.js +113 -4
  330. package/dist/plugins/k8s-bg-scripts.d.ts +34 -0
  331. package/dist/plugins/k8s-bg-scripts.js +88 -4
  332. package/dist/plugins/k8s-exec-protocol.d.ts +33 -0
  333. package/dist/plugins/k8s-exec-protocol.js +41 -0
  334. package/dist/plugins/local-checkpoint-store.d.ts +40 -0
  335. package/dist/plugins/local-checkpoint-store.js +86 -7
  336. package/dist/plugins/local-session-store.d.ts +157 -0
  337. package/dist/plugins/local-session-store.js +289 -34
  338. package/dist/plugins/local-task-attachment-store.js +14 -2
  339. package/dist/plugins/mailbox-store-sql.d.ts +38 -0
  340. package/dist/plugins/mailbox-store-sql.js +39 -6
  341. package/dist/plugins/memory-engine-pg.d.ts +30 -0
  342. package/dist/plugins/memory-engine-pg.js +122 -4
  343. package/dist/plugins/memory-engine-tidb.d.ts +26 -0
  344. package/dist/plugins/memory-engine-tidb.js +115 -5
  345. package/dist/plugins/memory-engine-vector-util.d.ts +5 -0
  346. package/dist/plugins/memory-engine-vector-util.js +5 -0
  347. package/dist/plugins/memory-resume-anchor-store.d.ts +8 -0
  348. package/dist/plugins/memory-resume-anchor-store.js +17 -0
  349. package/dist/plugins/memory-run-store.d.ts +63 -0
  350. package/dist/plugins/memory-run-store.js +73 -8
  351. package/dist/plugins/memory-session-policy-store.d.ts +17 -0
  352. package/dist/plugins/memory-session-policy-store.js +17 -0
  353. package/dist/plugins/memory-sync-store-pg.d.ts +42 -0
  354. package/dist/plugins/memory-sync-store-pg.js +45 -2
  355. package/dist/plugins/memory-sync-store-tidb.d.ts +9 -0
  356. package/dist/plugins/memory-sync-store-tidb.js +31 -1
  357. package/dist/plugins/outcome-ledger-sql.d.ts +63 -0
  358. package/dist/plugins/outcome-ledger-sql.js +97 -3
  359. package/dist/plugins/pg-approval-store.d.ts +7 -0
  360. package/dist/plugins/pg-approval-store.js +7 -0
  361. package/dist/plugins/pg-breaker-state.d.ts +6 -0
  362. package/dist/plugins/pg-breaker-state.js +6 -0
  363. package/dist/plugins/pg-checkpoint-store.d.ts +8 -0
  364. package/dist/plugins/pg-checkpoint-store.js +8 -0
  365. package/dist/plugins/pg-cost-quota.d.ts +17 -0
  366. package/dist/plugins/pg-cost-quota.js +6 -0
  367. package/dist/plugins/pg-file-snapshot-store.d.ts +6 -0
  368. package/dist/plugins/pg-file-snapshot-store.js +6 -0
  369. package/dist/plugins/pg-image-bake.d.ts +9 -0
  370. package/dist/plugins/pg-image-bake.js +9 -0
  371. package/dist/plugins/pg-image-index.d.ts +9 -0
  372. package/dist/plugins/pg-image-index.js +9 -0
  373. package/dist/plugins/pg-outcome-ledger.d.ts +9 -0
  374. package/dist/plugins/pg-outcome-ledger.js +9 -0
  375. package/dist/plugins/pg-pool.d.ts +50 -0
  376. package/dist/plugins/pg-pool.js +78 -0
  377. package/dist/plugins/pg-query.d.ts +11 -0
  378. package/dist/plugins/pg-rate-limiter.d.ts +23 -0
  379. package/dist/plugins/pg-rate-limiter.js +12 -2
  380. package/dist/plugins/pg-resume-anchor-store.d.ts +5 -0
  381. package/dist/plugins/pg-resume-anchor-store.js +5 -0
  382. package/dist/plugins/pg-run-store.d.ts +7 -0
  383. package/dist/plugins/pg-run-store.js +7 -0
  384. package/dist/plugins/pg-safe-json.d.ts +13 -0
  385. package/dist/plugins/pg-safe-json.js +40 -4
  386. package/dist/plugins/pg-session-policy-store.d.ts +5 -0
  387. package/dist/plugins/pg-session-policy-store.js +5 -0
  388. package/dist/plugins/pg-session-storage.d.ts +72 -0
  389. package/dist/plugins/pg-session-storage.js +152 -20
  390. package/dist/plugins/pg-session-store.d.ts +10 -0
  391. package/dist/plugins/pg-session-store.js +10 -0
  392. package/dist/plugins/pg-tool-result-store.d.ts +7 -0
  393. package/dist/plugins/pg-tool-result-store.js +7 -0
  394. package/dist/plugins/pg-workflow-journal-store.d.ts +7 -0
  395. package/dist/plugins/pg-workflow-journal-store.js +7 -0
  396. package/dist/plugins/pg-workflow-run-store.d.ts +7 -0
  397. package/dist/plugins/pg-workflow-run-store.js +7 -0
  398. package/dist/plugins/posix-shell-fs.d.ts +3 -0
  399. package/dist/plugins/posix-shell-fs.js +31 -1
  400. package/dist/plugins/remote-env-adb.d.ts +25 -0
  401. package/dist/plugins/remote-env-adb.js +95 -12
  402. package/dist/plugins/remote-env-e2b.d.ts +146 -0
  403. package/dist/plugins/remote-env-e2b.js +371 -40
  404. package/dist/plugins/remote-env-file-error.d.ts +34 -0
  405. package/dist/plugins/remote-env-file-error.js +37 -0
  406. package/dist/plugins/remote-env-host.d.ts +151 -0
  407. package/dist/plugins/remote-env-host.js +468 -70
  408. package/dist/plugins/remote-env-k8s.d.ts +135 -0
  409. package/dist/plugins/remote-env-k8s.js +318 -32
  410. package/dist/plugins/remote-env-local-docker.d.ts +65 -0
  411. package/dist/plugins/remote-env-local-docker.js +149 -24
  412. package/dist/plugins/remote-env-ssh.d.ts +35 -0
  413. package/dist/plugins/remote-env-ssh.js +117 -20
  414. package/dist/plugins/remote-scratchpad.d.ts +38 -0
  415. package/dist/plugins/remote-scratchpad.js +32 -2
  416. package/dist/plugins/remote-shell.d.ts +22 -0
  417. package/dist/plugins/remote-shell.js +27 -1
  418. package/dist/plugins/resume-anchor-store-sql.d.ts +28 -0
  419. package/dist/plugins/resume-anchor-store-sql.js +11 -0
  420. package/dist/plugins/roster-store-sql.d.ts +27 -0
  421. package/dist/plugins/roster-store-sql.js +54 -0
  422. package/dist/plugins/run-store-sql.d.ts +194 -0
  423. package/dist/plugins/run-store-sql.js +210 -9
  424. package/dist/plugins/s3-presign.d.ts +20 -0
  425. package/dist/plugins/s3-presign.js +49 -2
  426. package/dist/plugins/scheduler-support.d.ts +12 -0
  427. package/dist/plugins/scheduler-support.js +75 -3
  428. package/dist/plugins/send-file-ledger.d.ts +15 -0
  429. package/dist/plugins/send-file-ledger.js +53 -4
  430. package/dist/plugins/send-user-file.d.ts +77 -0
  431. package/dist/plugins/send-user-file.js +92 -4
  432. package/dist/plugins/session-policy-store-sql.d.ts +15 -0
  433. package/dist/plugins/session-policy-store-sql.js +75 -3
  434. package/dist/plugins/session-store.d.ts +45 -0
  435. package/dist/plugins/session-store.js +51 -0
  436. package/dist/plugins/sql-driver.d.ts +77 -0
  437. package/dist/plugins/sql-driver.js +4 -0
  438. package/dist/plugins/sql-escape.d.ts +8 -0
  439. package/dist/plugins/sql-escape.js +8 -0
  440. package/dist/plugins/sql-row-helpers.d.ts +25 -0
  441. package/dist/plugins/sql-row-helpers.js +25 -0
  442. package/dist/plugins/store-backend.d.ts +103 -0
  443. package/dist/plugins/store-backend.js +120 -27
  444. package/dist/plugins/store-contracts.d.ts +62 -0
  445. package/dist/plugins/store-contracts.js +3 -0
  446. package/dist/plugins/task-attachment-store.d.ts +60 -0
  447. package/dist/plugins/task-attachment-store.js +49 -3
  448. package/dist/plugins/task-list-store-sql.d.ts +25 -0
  449. package/dist/plugins/task-list-store-sql.js +21 -0
  450. package/dist/plugins/tidb-approval-store.d.ts +6 -0
  451. package/dist/plugins/tidb-approval-store.js +6 -0
  452. package/dist/plugins/tidb-breaker-state.d.ts +5 -0
  453. package/dist/plugins/tidb-breaker-state.js +5 -0
  454. package/dist/plugins/tidb-checkpoint-store.d.ts +7 -0
  455. package/dist/plugins/tidb-checkpoint-store.js +7 -0
  456. package/dist/plugins/tidb-cost-quota.d.ts +6 -0
  457. package/dist/plugins/tidb-cost-quota.js +1 -0
  458. package/dist/plugins/tidb-file-snapshot-store.d.ts +6 -0
  459. package/dist/plugins/tidb-file-snapshot-store.js +6 -0
  460. package/dist/plugins/tidb-image-bake.d.ts +8 -0
  461. package/dist/plugins/tidb-image-bake.js +8 -0
  462. package/dist/plugins/tidb-image-index.d.ts +8 -0
  463. package/dist/plugins/tidb-image-index.js +8 -0
  464. package/dist/plugins/tidb-outcome-ledger.d.ts +10 -0
  465. package/dist/plugins/tidb-outcome-ledger.js +10 -0
  466. package/dist/plugins/tidb-pool.d.ts +56 -0
  467. package/dist/plugins/tidb-pool.js +181 -1
  468. package/dist/plugins/tidb-rate-limiter.d.ts +11 -0
  469. package/dist/plugins/tidb-rate-limiter.js +5 -2
  470. package/dist/plugins/tidb-resume-anchor-store.d.ts +5 -0
  471. package/dist/plugins/tidb-resume-anchor-store.js +5 -0
  472. package/dist/plugins/tidb-run-store.d.ts +7 -0
  473. package/dist/plugins/tidb-run-store.js +7 -0
  474. package/dist/plugins/tidb-session-policy-store.d.ts +5 -0
  475. package/dist/plugins/tidb-session-policy-store.js +5 -0
  476. package/dist/plugins/tidb-session-storage.d.ts +43 -0
  477. package/dist/plugins/tidb-session-storage.js +62 -3
  478. package/dist/plugins/tidb-session-store.d.ts +142 -0
  479. package/dist/plugins/tidb-session-store.js +264 -16
  480. package/dist/plugins/tidb-tool-result-store.d.ts +5 -0
  481. package/dist/plugins/tidb-tool-result-store.js +7 -0
  482. package/dist/plugins/tidb-workflow-journal-store.d.ts +7 -0
  483. package/dist/plugins/tidb-workflow-journal-store.js +7 -0
  484. package/dist/plugins/tidb-workflow-run-store.d.ts +8 -0
  485. package/dist/plugins/tidb-workflow-run-store.js +8 -0
  486. package/dist/plugins/tool-result-store-sql.d.ts +58 -0
  487. package/dist/plugins/tool-result-store-sql.js +38 -2
  488. package/dist/plugins/web-search.d.ts +35 -0
  489. package/dist/plugins/web-search.js +45 -9
  490. package/dist/plugins/workflow-journal-limits.d.ts +10 -0
  491. package/dist/plugins/workflow-journal-limits.js +10 -0
  492. package/dist/plugins/workflow-journal-store-sql.d.ts +78 -0
  493. package/dist/plugins/workflow-journal-store-sql.js +58 -2
  494. package/dist/plugins/workflow-run-store-sql.d.ts +84 -0
  495. package/dist/plugins/workflow-run-store-sql.js +84 -6
  496. package/dist/plugins/worktree-isolation.d.ts +45 -0
  497. package/dist/plugins/worktree-isolation.js +105 -6
  498. package/dist/plugins/write-behind-counter.d.ts +56 -1
  499. package/dist/plugins/write-behind-counter.js +53 -14
  500. package/dist/principal-jwt.d.ts +24 -0
  501. package/dist/principal-jwt.js +60 -5
  502. package/dist/project-memory.d.ts +22 -0
  503. package/dist/project-memory.js +144 -15
  504. package/dist/prompts-domain-validate.d.ts +28 -0
  505. package/dist/prompts-domain-validate.js +42 -1
  506. package/dist/question.d.ts +33 -0
  507. package/dist/question.js +69 -2
  508. package/dist/resource-suspend.d.ts +21 -0
  509. package/dist/resource-suspend.js +16 -0
  510. package/dist/router/route-orchestration.d.ts +78 -0
  511. package/dist/router/route-orchestration.js +77 -0
  512. package/dist/run-local.d.ts +32 -0
  513. package/dist/run-local.js +172 -8
  514. package/dist/runs.d.ts +189 -4
  515. package/dist/runs.js +465 -24
  516. package/dist/runtime-caps-resolver.d.ts +106 -0
  517. package/dist/runtime-caps-resolver.js +89 -4
  518. package/dist/runtime-governance.d.ts +84 -0
  519. package/dist/runtime-governance.js +127 -1
  520. package/dist/sandbox-pkg-source.d.ts +23 -0
  521. package/dist/sandbox-pkg-source.js +37 -0
  522. package/dist/sealed-key.d.ts +54 -0
  523. package/dist/sealed-key.js +68 -3
  524. package/dist/security.d.ts +108 -0
  525. package/dist/security.js +145 -6
  526. package/dist/sema-registry.d.ts +21 -0
  527. package/dist/sema-registry.js +21 -0
  528. package/dist/session-leaf-bus.d.ts +31 -0
  529. package/dist/session-leaf-bus.js +32 -0
  530. package/dist/session-sync-content.d.ts +46 -0
  531. package/dist/session-sync-content.js +47 -1
  532. package/dist/session-sync-kernel.d.ts +89 -0
  533. package/dist/session-sync-kernel.js +48 -3
  534. package/dist/session-sync.d.ts +125 -0
  535. package/dist/session-sync.js +181 -8
  536. package/dist/session-titler.d.ts +26 -0
  537. package/dist/session-titler.js +41 -7
  538. package/dist/session-watch.d.ts +53 -1
  539. package/dist/session-watch.js +73 -9
  540. package/dist/sighup-idle.d.ts +30 -0
  541. package/dist/sighup-idle.js +12 -1
  542. package/dist/spec-fields.d.ts +91 -3
  543. package/dist/spec-fields.js +116 -4
  544. package/dist/task-cwd.d.ts +50 -0
  545. package/dist/task-cwd.js +68 -1
  546. package/dist/task-mcp.d.ts +47 -0
  547. package/dist/task-mcp.js +29 -0
  548. package/dist/task-settings.d.ts +175 -0
  549. package/dist/task-settings.js +226 -11
  550. package/dist/task-workflow.d.ts +67 -0
  551. package/dist/task-workflow.js +75 -8
  552. package/dist/tool-approval.d.ts +91 -0
  553. package/dist/tool-approval.js +215 -0
  554. package/dist/trace/artifacts.d.ts +9 -0
  555. package/dist/trace/artifacts.js +30 -5
  556. package/dist/trace/core-keyset-guard.d.ts +18 -0
  557. package/dist/trace/ledger-sink.d.ts +43 -0
  558. package/dist/trace/ledger-sink.js +64 -5
  559. package/dist/trace/project.d.ts +133 -0
  560. package/dist/trace/project.js +189 -2
  561. package/dist/trace/redact.d.ts +19 -0
  562. package/dist/trace/redact.js +66 -9
  563. package/dist/usage-analytics.d.ts +26 -0
  564. package/dist/usage-analytics.js +26 -3
  565. package/package.json +1 -1
package/dist/config.js CHANGED
@@ -3,7 +3,7 @@ import { join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { CODE_AGENT_PROMPT, formatUserScope, isThinkingLevel, RECOMMENDED_SENSITIVE_PATTERNS } from "@sema-agent/core";
5
5
  import { ROSTER_PRIMARY_ROLES, ROSTER_CHEAP_ROLES } from "@sema-agent/registry-core";
6
- import { parseApprovalHmacKeys, parsePrincipalJwks } from "./auth-keys.js";
6
+ import { parseApprovalHmacKeys, parsePrincipalJwks } from "./auth-keys.js"; // design/158 A4: the parser leaf — NOT security.js (base config layer must not value-import the 55KiB auth module)
7
7
  import { DEFAULT_ELICITATION_THROTTLE } from "./elicitation.js";
8
8
  function csv(name) {
9
9
  return (process.env[name] ?? "")
@@ -12,6 +12,11 @@ function csv(name) {
12
12
  .filter(Boolean);
13
13
  }
14
14
  const AUTONOMY_MODES = ["read-only", "ask", "plan", "auto"];
15
+ /** Parse the AUTONOMY env into a validated autonomy mode. Unset/empty → undefined (unmanaged → no extra
16
+ * tightening). An UNKNOWN value FAILS at startup rather than silently becoming a no-op (a typo'd `AUTONOMY=readonly`
17
+ * must not silently leave a deployment ungoverned — fail-loud, same discipline as numEnv). Exported so the HOT
18
+ * config overlay (sema-registry.ts `applyRuntimeHot`) can re-derive the ENV BASELINE to revert to when center
19
+ * stops managing `autonomy` (a stale center override must not stick — see applyRuntimeHot). */
15
20
  export function parseAutonomy(raw) {
16
21
  const v = raw?.trim();
17
22
  if (!v)
@@ -21,6 +26,9 @@ export function parseAutonomy(raw) {
21
26
  }
22
27
  return v;
23
28
  }
29
+ /** Resolve a CSV of env-var NAMES into a {NAME: value} map from THIS process's env (out-of-band secret
30
+ * forwarding into a sandbox). A name with no value set is skipped (not forwarded as undefined). The values
31
+ * are secrets — they live only in the returned map (in-memory), never logged. */
24
32
  function resolveEnvForward(csvNames) {
25
33
  const out = {};
26
34
  for (const name of csvNames.split(",").map((s) => s.trim()).filter(Boolean)) {
@@ -39,14 +47,21 @@ function env(name, fallback) {
39
47
  }
40
48
  return v;
41
49
  }
50
+ /** 出厂 skill 目录的缺省 = 包内 `skills/`(相对本模块解析),不是 cwd 相对的裸 "skills"。
51
+ * 真 bug(live 战役 2026-07-15 抓获):npm 全局装的 CLE 从用户 cwd 起引擎,裸 "skills" 解析到
52
+ * cwd/skills(不存在)⇒ skills:0,所有烤入 skill 在 npm 分发面全空转(bench 日志 skills:0 即此)。
53
+ * dist/config.js 与 skills/ 同在包根下的 ../skills;src 跑测试时 ../skills 同样是仓根 skills(一致)。
54
+ * SKILLS_DIR 显式设仍覆盖此缺省;Docker(WORKDIR 包根+COPY skills)解析到 /app/skills 不破。 */
42
55
  function defaultSkillsDir() {
43
56
  try {
44
57
  return fileURLToPath(new URL("../skills", import.meta.url));
45
58
  }
46
59
  catch {
47
- return "skills";
60
+ return "skills"; // import.meta 不可用的兜底(不应发生在 ESM 构建里)
48
61
  }
49
62
  }
63
+ /** Two-name env with the first set winning (MYSQL_* preferred over the TIDB_* back-compat alias); throws
64
+ * when neither is set and no fallback is given — same fail-loud contract as `env`. */
50
65
  function env2(primary, alias, fallback) {
51
66
  const v = process.env[primary] || process.env[alias];
52
67
  if (v === undefined || v === "") {
@@ -56,6 +71,8 @@ function env2(primary, alias, fallback) {
56
71
  }
57
72
  return v;
58
73
  }
74
+ /** Numeric env with validation: a non-numeric value FAILS at startup instead of silently becoming `NaN`
75
+ * (which e.g. slips past the `runStaleSec` liveness guard and silently disables the reaper). */
59
76
  function numEnv(name, fallback) {
60
77
  const raw = env(name, fallback);
61
78
  const n = Number(raw);
@@ -63,25 +80,51 @@ function numEnv(name, fallback) {
63
80
  throw new Error(`env ${name}="${raw}" must be a number`);
64
81
  return n;
65
82
  }
83
+ /** Numeric env with a [min,max] bound (BL-17): an out-of-range value FAILS at startup instead of being
84
+ * silently clamped/applied (e.g. a multi-hour MCP_ELICITATION_TTL_MS would silently never expire). */
66
85
  function numEnvBounded(name, fallback, min, max) {
67
86
  const n = numEnv(name, fallback);
68
87
  if (n < min || n > max)
69
88
  throw new Error(`env ${name}=${n} must be in [${min}, ${max}]`);
70
89
  return n;
71
90
  }
91
+ /** Optional FAIL-SAFE numeric env for an OPTIONAL feature knob: a non-numeric / non-positive value yields `undefined`
92
+ * (the caller falls back to its own default) rather than NaN OR a hard startup throw. Used for SESSION_SNAPSHOT_TTL_SEC
93
+ * (finding 20): a typo like "600s" must NOT sign every presigned URL with X-Amz-Expires=NaN (total snapshot outage),
94
+ * but ALSO must not crash boot of an unrelated subsystem — degrade to the MinioBlobBackend default. */
95
+ /** S20 (SILENT-FALLBACK P1): soft-knob parse rejects, collected here because config parsing runs BEFORE the
96
+ * logger exists. main.ts drains this once after createLogger and warns per entry ("configured X=raw is
97
+ * invalid, using the default"). Module-level on purpose; drain clears it (repeat loadConfig in tests is fine). */
72
98
  const CONFIG_WARNINGS = [];
73
99
  export function drainConfigWarnings() {
74
100
  return CONFIG_WARNINGS.splice(0, CONFIG_WARNINGS.length);
75
101
  }
102
+ /** Boot NOTICES ([792]④): non-error decisions loadConfig makes that the operator should see once (e.g. the
103
+ * MODEL_PROVIDER inference below). Same pre-logger collection pattern as CONFIG_WARNINGS, but each entry
104
+ * carries its own event name + fields (the warnings drain has one fixed "invalid, using default" message
105
+ * which would misdescribe an inference). main.ts drains + logs right after createLogger. */
76
106
  const CONFIG_NOTICES = [];
77
107
  export function drainConfigNotices() {
78
108
  return CONFIG_NOTICES.splice(0, CONFIG_NOTICES.length);
79
109
  }
110
+ /** F10 (codex audit): drain + log BOTH pre-logger diagnostic collectors (S20 soft-knob warnings + [792]④ boot
111
+ * notices) in one place. EVERY entrypoint that calls loadConfig() then builds a logger must call this right
112
+ * after (main.ts, run-local.ts) — a new entry that forgets it would silently drop operator-facing diagnostics
113
+ * (run-local historically did exactly that), so the drain lives beside the collectors, not in one entrypoint. */
80
114
  export function logConfigDiagnostics(logger) {
81
115
  for (const w of drainConfigWarnings())
82
116
  logger.warn("config_env_invalid_using_default", { env: w.env, raw: w.raw });
83
117
  for (const n of drainConfigNotices())
84
118
  logger.warn(n.event, n.fields);
119
+ // design/158 N9 (polarity self-report): a `*_ENABLED` suffix does NOT carry its default — the repo grew four
120
+ // dialects for it (opt-in / opt-out / posture tri-state / negative name). Rather than force operators to read
121
+ // config.ts to learn what a knob does when unset, boolEnv-parsed knobs register themselves and we print one
122
+ // debug line per knob: name → polarity → effective value → where that value came from. `debug` is optional so
123
+ // an older embedder-supplied logger shape (warn-only) still compiles and still gets the warnings above.
124
+ // 复审 2026-07-29 #6 — the table covers a LOAD, not the catalog: five boolEnv calls sit inside conditional
125
+ // config shapes (E2B_ALLOW_NET only when set; K8S_INSECURE_TLS / DOCKER_DROP_CAPS only under their provider
126
+ // branch; MODEL_DEGRADE_REACTIVE / MODEL_DEGRADE_TO_VISION only when MODEL_DEGRADE_TO is set) and register
127
+ // only when that shape is active — absence of a row means "not a live knob in this config", not "off".
85
128
  if (logger.debug) {
86
129
  for (const k of configKnobTable()) {
87
130
  logger.debug("config_knob_polarity", {
@@ -94,6 +137,11 @@ export function logConfigDiagnostics(logger) {
94
137
  }
95
138
  }
96
139
  }
140
+ /** F8 (codex audit): first NON-EMPTY env among aliased names — "" = unset, the same convention `env()` uses.
141
+ * A plain `??` chain treats an EMPTY first choice as SET and shadows a configured fallback (e.g.
142
+ * `ANTHROPIC_BASEURL=""` from a compose `${VAR:-}` template hiding a real `ANTHROPIC_BASE_URL`, or an empty
143
+ * `ANTHROPIC_API_KEY` suppressing the whole route despite a valid `ANTHROPIC_AUTH_TOKEN`). Shared by the
144
+ * MODEL_PROVIDER inference AND the anthropic route assembly so the two predicates can never drift. */
97
145
  function firstSetEnv(...names) {
98
146
  for (const n of names) {
99
147
  const v = process.env[n];
@@ -108,13 +156,18 @@ function optFinitePositiveEnv(name) {
108
156
  return undefined;
109
157
  const n = Number(raw);
110
158
  if (!(Number.isFinite(n) && n > 0)) {
111
- CONFIG_WARNINGS.push({ env: name, raw });
159
+ CONFIG_WARNINGS.push({ env: name, raw }); // S20: previously a silent swallow — caller quietly used the default
112
160
  return undefined;
113
161
  }
114
162
  return n;
115
163
  }
164
+ /** Keyed by canonical name so a knob read twice in one load (e.g. DURABLE_APPROVAL, once for the D-G boot
165
+ * invariant and once for the returned field) registers one row, not two. Reset per `loadConfig()`. */
116
166
  const CONFIG_KNOBS = new Map();
167
+ /** Deprecated aliases already announced for this load — the lazily-read twins (HOST_BG/HOST_EXEC_SPOOL) resolve
168
+ * again on every use, and an operator does not need the same deprecation line once per command. */
117
169
  const KNOB_DEPRECATIONS_SEEN = new Set();
170
+ /** The registered boolean knobs, sorted by name — the data behind `logConfigDiagnostics`'s polarity table. */
118
171
  export function configKnobTable() {
119
172
  return [...CONFIG_KNOBS.values()].sort((a, b) => a.name.localeCompare(b.name));
120
173
  }
@@ -126,6 +179,7 @@ function boolEnv(name, def) {
126
179
  let value = def;
127
180
  let source = "default";
128
181
  if (raw !== undefined && raw !== "") {
182
+ // "" = unset, the same convention `env()`/`firstSetEnv()` use (a compose `${VAR:-}` template passes empty through).
129
183
  if (raw === "true") {
130
184
  value = true;
131
185
  source = "env";
@@ -135,12 +189,31 @@ function boolEnv(name, def) {
135
189
  source = "env";
136
190
  }
137
191
  else if (!CONFIG_KNOBS.has(name)) {
192
+ // Previously a silent default at every hand-written site. Guarded on first registration: the LAZILY
193
+ // re-read knobs (boolEnvWithLegacyNegated's host twins, read per exec) must not grow this array for the
194
+ // process lifetime on one typo — one typo, one warning. (复审 2026-07-30 F6:早期理由「IMAGE_BAKES_
195
+ // ENABLED/DURABLE_APPROVAL 一次 load 读两次」已随九域拆分的去重失效,懒读族成为本守卫的现行理由。)
138
196
  CONFIG_WARNINGS.push({ env: name, raw });
139
197
  }
140
198
  }
141
199
  registerKnob({ name, polarity: def ? "opt-out" : "opt-in", value, source });
142
200
  return value;
143
201
  }
202
+ /** design/158 N8 (D-family) — a knob whose CANONICAL name is positive (`X_ENABLED`, default ON) but which keeps
203
+ * its retired negative name (`X_DISABLED`) working for one deprecation window.
204
+ *
205
+ * Why the flip at all: `X_DISABLED !== "true"` is a double negative at the read site, so the four knobs that
206
+ * used it (`PROJECT_MEMORY`, `CONFIG_LKG`, `HOST_BG`, `HOST_EXEC_SPOOL`) could not be read for their default
207
+ * without inverting twice in your head, and `projectMemoryDisabled` was the ONE negative boolean on a
208
+ * `ServiceConfig` that otherwise carries 16 positive `*Enabled`/`*Allowed` fields.
209
+ *
210
+ * Resolution order (new name wins, so there is never an ambiguous both-set state):
211
+ * 1. `X_ENABLED` set to a literal ⇒ that.
212
+ * 2. `X_ENABLED` set to a non-literal ⇒ default + warning (the legacy name is NOT consulted — a typo in the
213
+ * new name must not silently hand control back to the deprecated one).
214
+ * 3. `X_ENABLED` unset ∧ `X_DISABLED` set ⇒ the legacy site's exact contract, `X_DISABLED !== "true"`, plus a
215
+ * one-shot deprecation notice naming the replacement.
216
+ * 4. neither ⇒ default. */
144
217
  export function boolEnvWithLegacyNegated(name, legacyNegatedName, def) {
145
218
  const raw = process.env[name];
146
219
  const legacy = process.env[legacyNegatedName];
@@ -152,13 +225,16 @@ export function boolEnvWithLegacyNegated(name, legacyNegatedName, def) {
152
225
  }
153
226
  else if (raw !== undefined && raw !== "") {
154
227
  if (!CONFIG_KNOBS.has(name)) {
228
+ // Same first-registration guard as boolEnv — the three consumers of this helper are the LAZILY re-read
229
+ // ones (remote-env-host reads per exec/probe), so an unguarded push would grow CONFIG_WARNINGS for the
230
+ // whole process lifetime on a single operator typo (复审 2026-07-29 #1).
155
231
  CONFIG_WARNINGS.push({ env: name, raw });
156
232
  }
157
233
  value = def;
158
234
  source = "default";
159
235
  }
160
236
  else if (legacy !== undefined && legacy !== "") {
161
- value = legacy !== "true";
237
+ value = legacy !== "true"; // verbatim legacy contract: only the literal "true" disabled the feature
162
238
  source = "legacy-env";
163
239
  if (!KNOB_DEPRECATIONS_SEEN.has(legacyNegatedName)) {
164
240
  KNOB_DEPRECATIONS_SEEN.add(legacyNegatedName);
@@ -175,33 +251,55 @@ export function boolEnvWithLegacyNegated(name, legacyNegatedName, def) {
175
251
  registerKnob({ name, polarity: def ? "opt-out" : "opt-in", value, source, legacyName: legacyNegatedName });
176
252
  return value;
177
253
  }
254
+ /** LKG (last-known-good) persistence of the remote effective-config fetch. Read LAZILY at both main.ts sites
255
+ * rather than lifted onto `ServiceConfig`, so this stays a pure env knob; `loadConfig` pre-registers it (below)
256
+ * so its polarity row and any deprecation notice appear at boot with everything else. */
178
257
  export const configLkgEnabled = () => boolEnvWithLegacyNegated("CONFIG_LKG_ENABLED", "CONFIG_LKG_DISABLED", true);
258
+ /** The `host` exec lane's background-shell capability kill switch (read per capability probe in remote-env-host.ts). */
179
259
  export const hostBackgroundShellEnabled = () => boolEnvWithLegacyNegated("HOST_BG_ENABLED", "HOST_BG_DISABLED", true);
260
+ /** The `host` exec lane's spool-file stdio form; false ⇒ the pre-1.226 pipe fallback (read per exec). */
180
261
  export const hostExecSpoolEnabled = () => boolEnvWithLegacyNegated("HOST_EXEC_SPOOL_ENABLED", "HOST_EXEC_SPOOL_DISABLED", true);
262
+ /** Enum env with validation: a typo (e.g. SESSION_BACKEND=Tidb) FAILS at startup instead of being silently
263
+ * cast to the union type and degrading to the default backend (dropping the persistence the operator asked for). */
181
264
  function enumEnv(name, fallback, allowed) {
182
265
  const v = (process.env[name] ?? fallback);
183
266
  if (!allowed.includes(v))
184
267
  throw new Error(`env ${name}="${v}" must be one of: ${allowed.join(", ")}`);
185
268
  return v;
186
269
  }
270
+ /** A single HTTP header-name env: it is both read as a request header AND interpolated into the CORS
271
+ * `access-control-allow-headers` list, so a value with a comma/space would split that list (and isn't a
272
+ * valid header name anyway). Reject anything but header-token chars at startup (council). */
187
273
  function headerNameEnv(name, fallback) {
188
274
  const v = env(name, fallback);
189
275
  if (!/^[A-Za-z0-9_-]+$/.test(v))
190
276
  throw new Error(`env ${name}="${v}" must be a single header name ([A-Za-z0-9_-]+)`);
191
277
  return v;
192
278
  }
279
+ /**
280
+ * Accept an SSH private key in whatever single-line form a secret store can hold. A PEM key is multi-line, but
281
+ * sema-registry's `POST /api/config/secrets` rejects newlines — so let the value arrive base64-encoded (the
282
+ * recommended self-serve form) or with escaped `\n`, and reconstruct the real PEM here for ssh2.
283
+ */
193
284
  function normalizeSshPrivateKey(raw) {
194
285
  const v = raw.trim();
195
286
  if (v.includes("BEGIN ") && v.includes("PRIVATE KEY")) {
196
- return v.includes("\n") ? raw : v.replace(/\\n/g, "\n");
287
+ return v.includes("\n") ? raw : v.replace(/\\n/g, "\n"); // already PEM (real newlines) or escaped-\n single line
197
288
  }
198
289
  try {
199
- return Buffer.from(v, "base64").toString("utf8");
290
+ return Buffer.from(v, "base64").toString("utf8"); // base64-of-PEM (newline-free → secrets-API friendly)
200
291
  }
201
292
  catch {
202
293
  return raw;
203
294
  }
204
295
  }
296
+ /**
297
+ * Build `Model.extraBody` (core 1.60 generation-tuning passthrough) from env, or undefined when nothing
298
+ * is set (so a deployment that sets none is byte-for-byte unaffected). MODEL_EXTRA_BODY (raw JSON) is the
299
+ * general escape hatch; MODEL_FREQUENCY_PENALTY / MODEL_PRESENCE_PENALTY are typed conveniences merged on
300
+ * top (they win over the same key in MODEL_EXTRA_BODY). Throws on malformed JSON — fail loud at boot, not
301
+ * a silently-ignored tuning knob.
302
+ */
205
303
  function buildExtraBody() {
206
304
  const body = {};
207
305
  const raw = process.env.MODEL_EXTRA_BODY;
@@ -226,6 +324,10 @@ function buildExtraBody() {
226
324
  body.presence_penalty = Number(pp);
227
325
  return Object.keys(body).length > 0 ? body : undefined;
228
326
  }
327
+ /** [818]① 1M dual-window derivation (core 1.289 `Model.autoCompactTokens`): an EXPLICIT positive value wins
328
+ * (a center roster may declare its own); else contextWindow>=1e6 derives the CC-aligned 967000; else the field
329
+ * is REMOVED (a spread-inherited value from a bigger-window base model would mis-trigger a small window).
330
+ * Shared by the env lane (main + cheap models) and the center lane (sema-registry toModel). */
229
331
  export function applyAutoCompactWindow(m, explicit) {
230
332
  const target = m;
231
333
  if (explicit !== undefined && Number.isFinite(explicit) && explicit > 0) {
@@ -237,23 +339,27 @@ export function applyAutoCompactWindow(m, explicit) {
237
339
  else
238
340
  delete target.autoCompactTokens;
239
341
  }
240
- export function loadConfig() {
241
- CONFIG_WARNINGS.length = 0;
242
- CONFIG_NOTICES.length = 0;
243
- CONFIG_KNOBS.clear();
244
- KNOB_DEPRECATIONS_SEEN.clear();
245
- const gatewayBaseUrl = env("MODEL_GATEWAY_BASEURL", "http://127.0.0.1:8000/v1");
246
- const modelId = env("MODEL_ID", "Qwen3.5-35B");
247
- const requirePrincipal = boolEnv("REQUIRE_PRINCIPAL", false);
248
- const singleUserTurnkey = !requirePrincipal;
249
- const postureOn = (name, infraReady = true) => {
250
- const envVal = process.env[name];
251
- const explicit = envVal === "true" ? true : envVal === "false" ? false : undefined;
252
- const value = explicit ?? (singleUserTurnkey && infraReady);
253
- registerKnob({ name, polarity: "posture", value, source: explicit === undefined ? "posture" : "env" });
254
- return value;
255
- };
342
+ /** 域:store(持久化)—— DB 引擎三态、session 后端、SQL coords、快照 BLOB / SendUserFile 对象存储。 */
343
+ function parseStoreDomain(ctx) {
344
+ const { requirePrincipal } = ctx; // 跨域入参①:多租户裸 boot 的 memory 缺省由它决定(见下方注释)
345
+ // `local` (clay 2026-06-25 seamless local↔cloud) = the DB-less in-memory/file StoreBackend, so a locally-run HTTP
346
+ // service serves the same contract a cloud worker does. It has no SQL host/coords (createStoreBackend builds it
347
+ // without tidb/pg), so it's always "reachable". (Parsed FIRST so the session/memory posture defaults below can
348
+ // key off the chosen engine.) Wording (clay 2026-07-06): canonical value `mysql` = any MySQL-protocol server
349
+ // (MySQL / TiDB / MariaDB); `tidb` accepted as a back-compat alias. TiDB stays fully supported and is the
350
+ // recommended MySQL-protocol engine when semantic memory is on (native VECTOR(dim) recall).
351
+ // clay (2026-07-27,[1845] 桌面撞 501 案后):**裸 boot 默认 durable(local file 店)**。
352
+ // 旧默认「纯内存」让每个宿主(cli fixture/桌面/web BFF)都要手工记得 DB_BACKEND=local,漏设 =
353
+ // 「跑完的任务重启就丢 + durable runs 501」——单机是主流形,直觉预期是留得住。三态:
354
+ // - 未设 local(单用户形;多租户例外见下)。默认推导的 local main.ts 侧 mkdir/ensure 失败
355
+ // 时**降级 memory + warn + gauge**(auto-DB 不可达先例),显式 DB_BACKEND=local fail-loud。
356
+ // - DB_BACKEND=memory ⇒ 显式纯内存 opt-out(CI/临时 server「不留盘上残余」的出口)。
357
+ // - 多租户裸 boot(REQUIRE_PRINCIPAL=true 且未设)⇒ 保持 memory + notice:local 与多租户互斥
358
+ // (main.ts 硬拒,owner map 无租户隔离),默认不得把多租户裸 boot 变成拒启。
256
359
  const dbBackendSet = !!process.env.DB_BACKEND;
360
+ // 「裸 boot」判定必须**无任何 SQL 信号**才成立——SESSION_BACKEND=mysql/auto 或 TIDB/MYSQL/PG coords
361
+ // 在场时,dbBackend 的旧默认 "mysql" 是**引擎选择器**(auto 探测/coords 构建都 key 它),默认切
362
+ // local 会把这些既有形错切到文件店(回归钉在 config-coverage)。
257
363
  const sqlSignal = (!!process.env.SESSION_BACKEND && process.env.SESSION_BACKEND !== "memory") ||
258
364
  !!(process.env.TIDB_HOST || process.env.MYSQL_HOST || process.env.PG_HOST);
259
365
  const bareBoot = !dbBackendSet && !sqlSignal;
@@ -268,49 +374,121 @@ export function loadConfig() {
268
374
  },
269
375
  });
270
376
  }
377
+ // Asymmetry收编 (clay 2026-07-06, overseas-pilot pit #1): an EXPLICIT `DB_BACKEND=mysql/tidb/pg` is the operator
378
+ // asking for a durable SQL deployment — default sessions onto it (fail-fast, same as an explicit
379
+ // SESSION_BACKEND=mysql) instead of silently keeping the in-memory default ("DB_BACKEND=pg engaged nothing,
380
+ // session:memory, PG 0 tables"). DB_BACKEND=local keeps its own coercion in main.ts (it needs the built
381
+ // backend); an UNSET DB_BACKEND keeps the memory default (no surprise DB requirement on a bare boot).
382
+ // `mysql` is accepted as an alias of the internal durable label `tidb` (see ServiceConfig.sessionBackend).
271
383
  const sqlEngineExplicit = !!process.env.DB_BACKEND && dbBackend !== "local" && dbBackend !== "memory";
272
384
  const sessionBackendRaw = enumEnv("SESSION_BACKEND", sqlEngineExplicit ? "mysql" : "memory", ["memory", "mysql", "tidb", "auto"]);
273
385
  const sessionBackend = sessionBackendRaw === "mysql" ? "tidb" : sessionBackendRaw;
274
- const memoryEngineEnabled = process.env.MEMORY_ENGINE !== "off";
275
- const memoryEngineBackendRaw = process.env.MEMORY_ENGINE_BACKEND ?? "file";
276
- if (!["file", "pg", "tidb"].includes(memoryEngineBackendRaw)) {
277
- throw new Error(`MEMORY_ENGINE_BACKEND must be file|pg|tidb, got "${memoryEngineBackendRaw}"`);
278
- }
279
- const memoryEngineBackend = memoryEngineBackendRaw;
280
- const workflowSizeGuidelineRaw = process.env.WORKFLOW_SIZE_GUIDELINE;
281
- if (workflowSizeGuidelineRaw !== undefined && !["small", "medium", "large", "unrestricted"].includes(workflowSizeGuidelineRaw)) {
282
- throw new Error(`WORKFLOW_SIZE_GUIDELINE must be small|medium|large|unrestricted, got "${workflowSizeGuidelineRaw}"`);
283
- }
284
- const workflowSizeGuideline = workflowSizeGuidelineRaw;
285
- const memoryScope = process.env.MEMORY_SCOPE ?? (memoryEngineEnabled && !requirePrincipal ? "local" : undefined);
286
- const memorySyncUrl = process.env.MEMORY_SYNC_URL || undefined;
287
- const memorySyncToken = process.env.MEMORY_SYNC_TOKEN || undefined;
288
- const memorySyncScopeRaw = process.env.MEMORY_SYNC_SCOPE || undefined;
289
- let memorySync;
290
- if (memorySyncUrl === undefined) {
291
- if (memorySyncToken !== undefined || memorySyncScopeRaw !== undefined) {
292
- throw new Error("MEMORY_SYNC_TOKEN/MEMORY_SYNC_SCOPE are set but MEMORY_SYNC_URL is not — refusing to start half-configured (set MEMORY_SYNC_URL to enable memory sync, or unset the other MEMORY_SYNC_* keys)");
293
- }
294
- }
295
- else {
296
- if (memoryEngineBackend !== "file") {
297
- throw new Error(`MEMORY_SYNC_URL is a FILE-memory (TOC client) knob, but MEMORY_ENGINE_BACKEND=${memoryEngineBackend} — the DB memory plane IS the central sync authority (it serves POST /v1/memory/sync/:scope) and must not also run the client leg; unset MEMORY_SYNC_* here`);
298
- }
299
- if (memorySyncToken === undefined) {
300
- throw new Error("MEMORY_SYNC_URL is set but MEMORY_SYNC_TOKEN is not — refusing to start half-configured (the central sync face requires a bearer token)");
301
- }
302
- const isV2ScopeKey = (k) => k.startsWith("user:") || k.startsWith("org:") || k.startsWith("proj:") || k.startsWith("userproj:");
303
- const scope = memorySyncScopeRaw ?? (memoryScope !== undefined ? (isV2ScopeKey(memoryScope) ? memoryScope : formatUserScope(memoryScope)) : undefined);
304
- if (scope === undefined) {
305
- throw new Error("MEMORY_SYNC_URL is set but no sync scope is derivable (memory engine off or multi-tenant without an explicit scope) — set MEMORY_SYNC_SCOPE, or run the single-user file memory engine so MEMORY_SCOPE (default \"local\") provides one");
306
- }
307
- memorySync = { url: memorySyncUrl, token: memorySyncToken, scope, ...((v) => (v !== undefined && Number.isInteger(v) && v >= 1 ? { maxPushEntries: v } : {}))(process.env.MEMORY_SYNC_MAX_PUSH_ENTRIES !== undefined ? Number(process.env.MEMORY_SYNC_MAX_PUSH_ENTRIES) : undefined), ...((v) => (v !== undefined && Number.isInteger(v) && v >= 1 ? { maxPullEntries: v } : {}))(process.env.MEMORY_SYNC_MAX_PULL_ENTRIES !== undefined ? Number(process.env.MEMORY_SYNC_MAX_PULL_ENTRIES) : undefined) };
308
- }
386
+ // P0.5 variant-2: the file-backed `local` backend's data root. SAME resolution main.ts:102 (localRoot =
387
+ // CONFIG_LOCAL_DIR ?? AGENT_DATA_DIR ?? ~/.ai-agent) + run-local.ts:198, so the HTTP service + a run-local on one
388
+ // box open ONE boot-locked data dir. `LOCAL_DATA_ROOT` is an additional highest-priority override (the explicit
389
+ // "the local backend's data lives HERE" knob). Only the `local` backend consumes it (createStoreBackend), but it's
390
+ // always resolved (cheap, env-deterministic).
309
391
  const localDataRoot = process.env.LOCAL_DATA_ROOT || process.env.CONFIG_LOCAL_DIR || process.env.AGENT_DATA_DIR || join(homedir(), ".ai-agent");
392
+ // `auto` builds the DB coords only when a host is actually configured — otherwise it stays memory with no
393
+ // probe. (Explicit mysql always builds them so a missing host fails loudly.) `needsDb` is engine-agnostic;
394
+ // the connection coords are built for whichever engine `dbBackend` selects (MYSQL_* preferred, TIDB_* alias).
310
395
  const dbHostSet = dbBackend === "pg" ? !!process.env.PG_HOST : !!(process.env.MYSQL_HOST || process.env.TIDB_HOST);
311
396
  const needsTidb = sessionBackend === "tidb" || (sessionBackend === "auto" && dbHostSet);
312
397
  const needsDb = needsTidb;
313
- const explicitProvider = process.env.MODEL_PROVIDER;
398
+ return {
399
+ sessionBackend,
400
+ sessionCacheTtlSec: Number(env("SESSION_CACHE_TTL_SEC", "300")),
401
+ rewindSnapshotMaxMb: optFinitePositiveEnv("REWIND_SNAPSHOT_MAX_MB"), // soft knob (S20: bad value warns + default)
402
+ dbBackend,
403
+ dbBackendExplicit: dbBackendSet,
404
+ localDataRoot,
405
+ tidb: needsDb && dbBackend === "mysql"
406
+ ? {
407
+ host: env2("MYSQL_HOST", "TIDB_HOST"),
408
+ // default port follows the env family in use: MYSQL_HOST → 3306 (stock MySQL), TIDB_HOST → 4000 (TiDB)
409
+ port: Number(env2("MYSQL_PORT", "TIDB_PORT", process.env.MYSQL_HOST ? "3306" : "4000")),
410
+ user: env2("MYSQL_USER", "TIDB_USER"),
411
+ password: env2("MYSQL_PASSWORD", "TIDB_PASSWORD", ""),
412
+ database: env2("MYSQL_DATABASE", "TIDB_DATABASE"),
413
+ connectionLimit: process.env.MYSQL_POOL_SIZE || process.env.TIDB_POOL_SIZE
414
+ ? Number(process.env.MYSQL_POOL_SIZE || process.env.TIDB_POOL_SIZE)
415
+ : undefined,
416
+ }
417
+ : undefined,
418
+ pg: needsDb && dbBackend === "pg"
419
+ ? {
420
+ host: env("PG_HOST"),
421
+ port: Number(env("PG_PORT", "5432")),
422
+ user: env("PG_USER"),
423
+ password: env("PG_PASSWORD", ""),
424
+ database: env("PG_DATABASE"),
425
+ connectionLimit: process.env.PG_POOL_SIZE ? Number(process.env.PG_POOL_SIZE) : undefined,
426
+ }
427
+ : undefined,
428
+ // S9 deeper fix: per-query DB timeout (see the interface doc). Soft knob — a typo degrades to the default
429
+ // posture (pool-wide off / counter-family 30s) with an S20 boot warning, never NaN into a driver timer.
430
+ dbQueryTimeoutMs: optFinitePositiveEnv("DB_QUERY_TIMEOUT_MS"),
431
+ // E19/2c snapshot-BLOB object-store offload (clay 2026-06-26). Reuses the SAME adapter-held MinIO creds as the k8s
432
+ // workspace-snapshot lane (secret minio-agent-worker); all three keys required to enable, else the file-snapshot
433
+ // stores keep the SQL `snapshot_blob` default (byte-identical to today). Bucket = SESSION_SNAPSHOT_BUCKET (default
434
+ // "session-snapshots"); keyPrefix optional (default "blobs/" in MinioBlobBackend).
435
+ snapshotBlobStore: process.env.MINIO_ENDPOINT && process.env.MINIO_ACCESS_KEY && process.env.MINIO_SECRET_KEY
436
+ ? {
437
+ endpoint: process.env.MINIO_ENDPOINT,
438
+ bucket: process.env.SESSION_SNAPSHOT_BUCKET ?? "session-snapshots",
439
+ accessKey: process.env.MINIO_ACCESS_KEY,
440
+ secretKey: process.env.MINIO_SECRET_KEY,
441
+ ...(process.env.MINIO_REGION ? { region: process.env.MINIO_REGION } : {}),
442
+ ...(process.env.SESSION_SNAPSHOT_PREFIX ? { keyPrefix: process.env.SESSION_SNAPSHOT_PREFIX } : {}),
443
+ // Fail-SAFE parse (finding 20): a non-numeric/non-positive value → omit (the MinioBlobBackend default 3600s
444
+ // applies), never NaN (which would sign every URL with X-Amz-Expires=NaN → total snapshot outage).
445
+ ...((ttl) => (ttl !== undefined ? { presignTtlSec: ttl } : {}))(optFinitePositiveEnv("SESSION_SNAPSHOT_TTL_SEC")),
446
+ }
447
+ : undefined,
448
+ // (a+c)(clay 2026-07-27)snapshot_blob 包墙修复两旋钮——语义见 ServiceConfig 字段注释。
449
+ snapshotBlobSqlMaxBytes: optFinitePositiveEnv("SNAPSHOT_BLOB_SQL_MAX_BYTES"),
450
+ snapshotBlobAllowSql: boolEnv("SNAPSHOT_BLOB_ALLOW_SQL_BYTES", false),
451
+ // SendUserFile 双轨公网直链(同三键开闸;S3_ENDPOINT 兼容位=外接 S3 场景内网/公网同址)。
452
+ // `||` 非 `??`(配置面自查 2026-07-14):env 惯例(env() helper 同款)空串=未设——`?? ` 会让
453
+ // 一个显式置空的 MINIO_ENDPOINT/S3_PUBLIC_ENDPOINT 把后备名吞掉(compose `${X:-}` 透传空串是常态)。
454
+ sendUserFile: (process.env.MINIO_ENDPOINT || process.env.S3_ENDPOINT) && process.env.MINIO_ACCESS_KEY && process.env.MINIO_SECRET_KEY
455
+ ? {
456
+ endpoint: (process.env.MINIO_ENDPOINT || process.env.S3_ENDPOINT),
457
+ ...(process.env.S3_PUBLIC_ENDPOINT || process.env.S3_ENDPOINT
458
+ ? { publicEndpoint: (process.env.S3_PUBLIC_ENDPOINT || process.env.S3_ENDPOINT) }
459
+ : {}),
460
+ publicBucket: process.env.S3_PUBLIC_BUCKET ?? "sema-public",
461
+ privateBucket: process.env.SESSION_SNAPSHOT_BUCKET ?? "session-snapshots",
462
+ privateKeyPrefix: "sendfile/",
463
+ ...(process.env.SEND_USER_FILE_SANDBOX_PUT_ENDPOINT ? { sandboxPutEndpoint: process.env.SEND_USER_FILE_SANDBOX_PUT_ENDPOINT } : {}),
464
+ accessKey: process.env.MINIO_ACCESS_KEY,
465
+ secretKey: process.env.MINIO_SECRET_KEY,
466
+ ...(process.env.MINIO_REGION ? { region: process.env.MINIO_REGION } : {}),
467
+ // fail-SAFE:非法值回落 0(永久轨缺省),不 NaN 进签名。
468
+ defaultTtlSec: ((v) => (v !== undefined && Number.isInteger(v) && v >= 0 ? v : 0))(process.env.SEND_USER_FILE_URL_TTL !== undefined ? Number(process.env.SEND_USER_FILE_URL_TTL) : undefined),
469
+ }
470
+ : undefined,
471
+ };
472
+ }
473
+ /** 域:model(模型面)—— 网关坐标、Anthropic 路线、韧性旋钮、主/廉价 model entry、role 表、降级梯。 */
474
+ function parseModelDomain() {
475
+ // npm 卫生纪律:默认值不烤内网坐标 — localhost 占位(公开分发正确缺省);真网关一律显式 env。
476
+ const gatewayBaseUrl = env("MODEL_GATEWAY_BASEURL", "http://127.0.0.1:8000/v1");
477
+ const modelId = env("MODEL_ID", "Qwen3.5-35B");
478
+ // Model.api must reflect the brain that actually serves it (routing is by provider): an "anthropic"
479
+ // provider runs the Anthropic brain, whose usage reports `input` EXCLUDING cached tokens. core ≥1.22
480
+ // normalizes the cache-hit-rate denominator per api family — a wrong api makes promptTokens too small
481
+ // and cacheHitRate exceed 100%. (Brains select by provider, not api, so this only fixes accounting.)
482
+ //
483
+ // Default inference ([792]④, core ruling 2026-07-14): an EXPLICIT MODEL_PROVIDER always wins (unchanged).
484
+ // When it is unset but the operator explicitly configured an Anthropic-protocol base URL AND a matching
485
+ // credential (the EXACT same `firstSetEnv` predicates the `anthropic` route assembly below uses — F8:
486
+ // non-empty selection, so an EMPTY first alias can't shadow a set fallback), the default is "anthropic" +
487
+ // a one-line boot notice — a clean-machine user pointing ANTHROPIC_BASE_URL at an Anthropic-compatible
488
+ // upstream must not have the model silently routed to the (unconfigured) openai gateway. Base URL alone or
489
+ // credential alone does NOT infer (a bare ANTHROPIC_API_KEY historically only assembles the cloud route for
490
+ // registry-declared anthropic models); neither present ⇒ "gateway", byte-identical to before.
491
+ const explicitProvider = process.env.MODEL_PROVIDER; // env() semantics: "" = unset
314
492
  const inferredAnthropicBaseUrl = firstSetEnv("ANTHROPIC_BASEURL", "ANTHROPIC_BASE_URL");
315
493
  const inferredAnthropicCred = firstSetEnv("ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN");
316
494
  const provider = (() => {
@@ -329,6 +507,12 @@ export function loadConfig() {
329
507
  }
330
508
  return "gateway";
331
509
  })();
510
+ // Generation-tuning passthrough (core 1.60 `Model.extraBody`, merged via applyExtraBody — brain-owned
511
+ // keys like temperature/max_tokens ALWAYS win, core warns on collision; unset → request unchanged).
512
+ // frequency/presence penalty are the common case (suppress degenerate looping at the source — the
513
+ // prevention to core's degenerate-repetition safety net); MODEL_EXTRA_BODY is the JSON escape hatch
514
+ // (top_k, logit_bias, …). MUST be STATIC (stable key order per cache namespace, else the prefix cache
515
+ // breaks — design/9/31), so it's built once here from fixed env, never per-task.
332
516
  const extraBody = buildExtraBody();
333
517
  const thinkingDefault = (() => { const v = process.env.MODEL_DEFAULT_THINKING; return v && isThinkingLevel(v) && v !== "off" ? v : undefined; })();
334
518
  const effortLevels = (process.env.MODEL_REASONING_EFFORT_LEVELS ?? "").split(",").map((s) => s.trim()).filter((s) => isThinkingLevel(s) && s !== "off");
@@ -337,9 +521,23 @@ export function loadConfig() {
337
521
  name: modelId,
338
522
  api: provider === "anthropic" ? "anthropic-messages" : "openai-completions",
339
523
  provider,
524
+ // Empty so the URL is owned by the BRAIN layer: each composed brain (gateway / failover fallback /
525
+ // Anthropic route) supplies its own baseUrl, and the brain resolves `model.baseUrl || config.baseUrl`.
526
+ // A non-empty value here would override every brain to one URL — breaking failover AND the Anthropic
527
+ // route (it would POST to the vLLM gateway). See design/15.
340
528
  baseUrl: "",
341
529
  reasoning: env("MODEL_REASONING", "true") === "true",
530
+ // vision precheck: the modality list drives BOTH the `model_no_vision` 422 (main.ts resolveSpec,
531
+ // when body.images hits a text-only model) AND the vision flag on GET /v1/models. The env-lane default model
532
+ // declares vision via MODEL_VISION — DEFAULT "true" keeps the historical image-capable behavior, so set
533
+ // MODEL_VISION=false for a text-only model (e.g. deepseek) to make the precheck actually fire instead of letting
534
+ // images透传 to an opaque downstream-gateway 400. (The centralized sema-registry lane derives input from its own
535
+ // per-model vision flag — see sema-registry.ts.)
342
536
  input: env("MODEL_VISION", "true") === "true" ? ["text", "image"] : ["text"],
537
+ // Per-1M-token USD pricing (core `modelCostToPricing`: input→inputPer1M, …). Default 0 keeps the
538
+ // historical behavior (spend reads $0) — set MODEL_COST_* on a deploy to make `model_cost_micro_usd`
539
+ // + the Fleet card's costUsd real. Numbers are the gateway's contract price (e.g. the review gateway's
540
+ // deepseek-pro), owned by the operator — the center never stores them (design/15: secrets+billing in env).
343
541
  cost: {
344
542
  input: Number(env("MODEL_COST_INPUT", "0")),
345
543
  output: Number(env("MODEL_COST_OUTPUT", "0")),
@@ -347,129 +545,134 @@ export function loadConfig() {
347
545
  cacheWrite: Number(env("MODEL_COST_CACHE_WRITE", "0")),
348
546
  },
349
547
  contextWindow: Number(env("MODEL_CONTEXT_WINDOW", "262144")),
548
+ // [854]③a: env 车道缺省 4096 → 32000。4096 对 reasoning 模型太小(一次完整思考+答案都放不下,长输出
549
+ // 任务被腰斩;上游同类缺省 32000)。per-model clamp:缺省值不越过本 entry 自己声明的 contextWindow
550
+ // (操作员配了小窗口模型时,缺省输出上限不得超过窗口本身 —— env 车道 model entry 就在此装配,唯一可用
551
+ // 约束即 MODEL_CONTEXT_WINDOW)。显式 MODEL_MAX_TOKENS 一如既往原样生效(显式配置=操作员意图,不 clamp);
552
+ // catalog(config.d/models.json)与 center 车道不动(那是显式配置,BL-8 继承链读的是这里的最终值)。
350
553
  maxTokens: process.env.MODEL_MAX_TOKENS !== undefined && process.env.MODEL_MAX_TOKENS !== ""
351
554
  ? Number(env("MODEL_MAX_TOKENS"))
352
555
  : Math.min(32000, Number(env("MODEL_CONTEXT_WINDOW", "262144"))),
556
+ // design/123 D2 (core 1.226): structural token-estimate coefficient (chars ≈ 1 token) feeding every
557
+ // non-usage-anchored estimate in core (compaction trigger fallback, clearStale/trim defenses, cut-point
558
+ // accounting). UNSET → omit = core default 4 (byte-compatible chars/4 heuristic). CJK-heavy models
559
+ // (deepseek on Chinese tasks) should set 2–3 — chars/4 underestimates Chinese by 60–75%, letting the
560
+ // request-layer defenses pass over-window requests. optFinitePositiveEnv: a typo'd value degrades to
561
+ // the safe core default rather than zeroing every structural estimate (soft knob, no fail-loud boot).
353
562
  ...(optFinitePositiveEnv("MODEL_CHARS_PER_TOKEN") !== undefined ? { charsPerToken: optFinitePositiveEnv("MODEL_CHARS_PER_TOKEN") } : {}),
563
+ // Cache family = the BRAIN's usage semantics (design/18): the Anthropic brain reports `input`
564
+ // EXCLUDING cached, the openai gateway brain INCLUDES it. Declare it EXPLICITLY (core 1.23
565
+ // `model.params.promptCacheFamily`) instead of leaving core to INFER it from `model.api` — cleaner,
566
+ // and `api` stays free to mean only "wire format". core warns + clamps if it's wrong (1.23).
354
567
  params: { promptCacheFamily: provider === "anthropic" ? "input-excludes-cached" : "input-includes-cached" },
568
+ // design/111 L2②: defaultThinking = this model's DEFAULT tier (resolution chain `spec.thinking ?? role.thinking ??
569
+ // model.defaultThinking`, prepare-task). reasoningEffortLevels (openai endpoints only — Anthropic budget honors the
570
+ // full range) DECLARES which tiers the endpoint accepts so a higher requested tier clamps DOWN instead of 422'ing.
355
571
  ...(thinkingDefault ? { defaultThinking: thinkingDefault } : {}),
356
572
  ...(effortLevels.length > 0 && provider !== "anthropic" ? { compat: { reasoningEffortLevels: effortLevels } } : {}),
357
573
  ...(extraBody ? { extraBody } : {}),
358
574
  };
575
+ // 1.24 role map: a catalog + role→model so subsystems resolve a ROLE (cost tiering) instead of a hard
576
+ // model name. An optional cheap model on the SAME gateway (MODEL_CHEAP_ID — just a different id) serves
577
+ // the `subagent`+`summarize` roles, so council lenses & compaction run cheap while the lead/arbiter/team
578
+ // stay on the main model. Unset MODEL_CHEAP_ID → every role resolves to the main model (unchanged).
359
579
  const cheapId = process.env.MODEL_CHEAP_ID;
360
580
  const cheapModel = cheapId
361
581
  ? {
362
582
  ...model,
363
583
  id: cheapId,
364
584
  name: cheapId,
585
+ // vision precheck (adversarial-review finding): the cheap model must declare its OWN vision
586
+ // capability. Spreading `...model` inherits the MAIN model's `input` modality, so a vision-capable main +
587
+ // text-only cheap (e.g. MODEL_CHEAP_ID=deepseek for subagent/summarize) would advertise vision on /v1/models
588
+ // AND defeat the precheck (config.models[cheapId] is user-pickable via body.model) — the SAME dead-code-on-a-lane
589
+ // class clay caught on the env main model. MODEL_CHEAP_VISION declares it (strict "true"); UNSET → inherit the
590
+ // main model's input (prior behavior, no change — the documented "same gateway, just a different id" case).
365
591
  input: process.env.MODEL_CHEAP_VISION === undefined
366
592
  ? model.input
367
593
  : process.env.MODEL_CHEAP_VISION === "true"
368
594
  ? ["text", "image"]
369
595
  : ["text"],
596
+ // Own pricing: pricing is keyed by model id (budget.buildPricing), so without this the cheap model
597
+ // inherits the MAIN model's `cost` and subagent/summarize spend (council lenses, compaction) is
598
+ // metered at the PRO per-token price → cost metric + per-principal quota over-count. Set
599
+ // MODEL_CHEAP_COST_* to the real cheap price; unset → inherit the main cost (prior behavior, no change).
370
600
  cost: {
371
601
  input: Number(process.env.MODEL_CHEAP_COST_INPUT ?? model.cost.input),
372
602
  output: Number(process.env.MODEL_CHEAP_COST_OUTPUT ?? model.cost.output),
373
603
  cacheRead: Number(process.env.MODEL_CHEAP_COST_CACHE_READ ?? model.cost.cacheRead),
374
604
  cacheWrite: Number(process.env.MODEL_CHEAP_COST_CACHE_WRITE ?? model.cost.cacheWrite),
375
605
  },
606
+ // systematic-audit: the cheap model also inherited the MAIN model's contextWindow/maxTokens/reasoning verbatim
607
+ // via the `...model` spread — wrong if MODEL_CHEAP_ID points at a model with different limits/capability. Declare
608
+ // them per cheap (MODEL_CHEAP_*); UNSET → inherit main (the documented "same gateway, just a different id" case).
376
609
  contextWindow: process.env.MODEL_CHEAP_CONTEXT_WINDOW ? Number(process.env.MODEL_CHEAP_CONTEXT_WINDOW) : model.contextWindow,
377
610
  maxTokens: process.env.MODEL_CHEAP_MAX_TOKENS ? Number(process.env.MODEL_CHEAP_MAX_TOKENS) : model.maxTokens,
611
+ // design/123 D2: charsPerToken per cheap slot — the spread inherits the MAIN model's coefficient,
612
+ // wrong when the cheap slot is a different tokenizer family (e.g. CJK-dense deepseek cheap under an
613
+ // anthropic main). MODEL_CHEAP_CHARS_PER_TOKEN overrides; UNSET → inherit main (same knob family).
378
614
  ...(optFinitePositiveEnv("MODEL_CHEAP_CHARS_PER_TOKEN") !== undefined ? { charsPerToken: optFinitePositiveEnv("MODEL_CHEAP_CHARS_PER_TOKEN") } : {}),
379
615
  reasoning: process.env.MODEL_CHEAP_REASONING === undefined ? model.reasoning : process.env.MODEL_CHEAP_REASONING === "true",
380
616
  }
381
617
  : undefined;
618
+ // [818]① 1M dual-window (core 1.289 `Model.autoCompactTokens`, additive): a 1M-window model gets
619
+ // autoCompactTokens=967000 (CC's 1M special-case = the autocompact window is 1M-33000; core then triggers at
620
+ // 967k-33k=934k with the guard on the PHYSICAL window — the exact CC alignment core recommends embedders set).
621
+ // Static catalog audit for this batch: ZERO baked 1M entries exist (env lane defaults 262144; center lane
622
+ // inherits/declares per roster) — so this is a DERIVATION on the declared window, not a data edit: any
623
+ // deployment that sets MODEL_CONTEXT_WINDOW>=1e6 (or a center roster with a 1M contextWindow —
624
+ // sema-registry.ts applies the same helper) gets the field automatically. <1M ⇒ field ABSENT (core's plain
625
+ // W-33000/0.7W geometry already matches CC there).
382
626
  applyAutoCompactWindow(model);
383
627
  if (cheapModel)
384
- applyAutoCompactWindow(cheapModel);
628
+ applyAutoCompactWindow(cheapModel); // the ...model spread must not leak the MAIN window's field onto a smaller cheap window
385
629
  const models = { default: model, [model.id]: model };
386
630
  if (cheapModel)
387
631
  models[cheapModel.id] = cheapModel;
388
632
  const cheapRef = cheapModel ? cheapModel.id : "default";
633
+ // M1(接入审计 2026-07-08):core verify 契约(verify.d.ts)把「verifier 必须异于 implementer」定为
634
+ // 🔴 部署契约——verifier 落 default(=implementer)时对抗验证者自评自批,去相关性名存实亡。
635
+ // `MODEL_VERIFIER=<catalog 模型 id>` 真兑现旧注释的 "operator can override"(此前只有注释没有机制);
636
+ // 值必须命中 catalog,typo 回落 default 并由 main.ts boot warn 曝光(连同"单模型部署未配 verifier"一起)。
389
637
  const verifierEnv = process.env.MODEL_VERIFIER;
390
638
  const verifierRef = verifierEnv && models[verifierEnv] ? verifierEnv : "default";
639
+ // rosterMarkerRoles 收敛(0.7 worker.model 真删同车):role→tier 的名单语义
640
+ // 单一来源 = registry-core 的 roster 标记常量(0.6.3 契约:ROSTER_PRIMARY_ROLES=["default","team","synthesize"],
641
+ // ROSTER_CHEAP_ROLES=["subagent","summarize"])——service 不再自带第二份「哪些 role 吃 cheap/primary」清单
642
+ // (双源并存期空转结束)。值仍指我方 catalog ref(cheap 槽未配 ⇒ cheapRef="default" 全落主模型,行为不变);
643
+ // `verifier` 是 service 专有 role(core verify 契约),不在 roster 标记语义内,保留显式。
391
644
  const roles = {
392
- verifier: verifierRef,
645
+ verifier: verifierRef, // adversarial verification gate (1.44) — MODEL_VERIFIER 指异构模型,未设=default(见 boot warn)
393
646
  };
394
647
  for (const r of ROSTER_PRIMARY_ROLES)
395
648
  roles[r] = "default";
396
649
  for (const r of ROSTER_CHEAP_ROLES)
397
650
  roles[r] = cheapRef;
651
+ // Developer-mode role preset (core 1.43, opt-in, default-neutral): attach the de-branded
652
+ // CODE_AGENT_PROMPT to the named roles via RoleSpec.systemPrompt — those roles run a coding prompt
653
+ // (when the task doesn't supply its own systemPrompt) while every other role keeps the neutral
654
+ // default. `MODEL_CODE_ROLES` unset → no change. e.g. MODEL_CODE_ROLES=default,subagent.
398
655
  for (const r of csv("MODEL_CODE_ROLES")) {
399
656
  const role = r;
400
657
  const cur = roles[role];
401
658
  if (cur === undefined)
402
- continue;
659
+ continue; // only decorate roles we actually define
660
+ // our role targets are model-name strings; keep an object target's fields if config ever sets one.
403
661
  roles[role] =
404
662
  typeof cur === "string"
405
663
  ? { model: cur, systemPrompt: CODE_AGENT_PROMPT }
406
664
  : { ...cur, systemPrompt: CODE_AGENT_PROMPT };
407
665
  }
408
- const directApprovalDoor = boolEnv("DIRECT_APPROVAL_DOOR", false);
409
- const principalJwtPubkeys = parsePrincipalJwks(process.env.PRINCIPAL_JWT_PUBKEYS);
410
- const principalJwtIss = process.env.PRINCIPAL_JWT_ISS || undefined;
411
- const principalJwtAud = process.env.PRINCIPAL_JWT_AUD || undefined;
412
- const principalJwtMaxTtlSec = Math.max(0, numEnv("PRINCIPAL_JWT_MAX_TTL_SEC", "120"));
413
- const dgHmacKeys = parseApprovalHmacKeys(process.env.APPROVAL_HMAC_KEYS);
414
- const dgOperators = csv("OPERATOR_PRINCIPALS");
415
- const dgDurable = boolEnv("DURABLE_APPROVAL", false);
416
- const directDoorActive = directApprovalDoor && dgDurable && dgHmacKeys.length > 0 && principalJwtPubkeys.length > 0 && !!principalJwtIss && !!principalJwtAud;
417
- if (directApprovalDoor) {
418
- const missing = [];
419
- if (!dgDurable)
420
- missing.push("DURABLE_APPROVAL=true (the direct door's crypto gate only exists on the durable /decide path)");
421
- if (dgHmacKeys.length === 0)
422
- missing.push("APPROVAL_HMAC_KEYS");
423
- if (principalJwtPubkeys.length === 0)
424
- missing.push("PRINCIPAL_JWT_PUBKEYS");
425
- if (!principalJwtIss)
426
- missing.push("PRINCIPAL_JWT_ISS");
427
- if (!principalJwtAud)
428
- missing.push("PRINCIPAL_JWT_AUD");
429
- if (dgOperators.length === 0)
430
- missing.push("OPERATOR_PRINCIPALS (else isOperator is true-for-all)");
431
- if (!requirePrincipal)
432
- missing.push("REQUIRE_PRINCIPAL=true (a direct door is multi-tenant; without it an absent principal is anonymous and the spoofable header governs owner/quota checks)");
433
- if (missing.length) {
434
- throw new Error(`DIRECT_APPROVAL_DOOR=true but D-G anchors are missing: ${missing.join(", ")} — refusing to start a half-open direct door (design/80 D-G boot invariant)`);
435
- }
436
- }
437
- const authTokens = Object.fromEntries((process.env.SERVICE_AUTH_TOKENS ?? "")
438
- .split(",")
439
- .map((s) => s.trim())
440
- .filter((s) => s.includes("="))
441
- .map((s) => [s.slice(0, s.indexOf("=")).trim(), s.slice(s.indexOf("=") + 1).trim()])
442
- .filter(([tok, sys]) => tok.length > 0 && sys.length > 0));
443
- const bakeRunnerPrincipal = env("BAKE_RUNNER_PRINCIPAL", "system:bake-runner");
444
- if (boolEnv("IMAGE_BAKES_ENABLED", false) && dgOperators.length === 0) {
445
- throw new Error("IMAGE_BAKES_ENABLED=true but OPERATOR_PRINCIPALS is empty — the bake door runs build.sh on the privileged build host; an empty operator set makes isOperator() true-for-all (world-writable RCE). Set OPERATOR_PRINCIPALS or disable bakes (IMAGE-API-DESIGN.md §P2.4a boot invariant)");
446
- }
447
- if (boolEnv("IMAGE_BAKES_ENABLED", false) && !Object.values(authTokens).includes(bakeRunnerPrincipal)) {
448
- throw new Error(`IMAGE_BAKES_ENABLED=true but no SERVICE_AUTH_TOKENS entry maps to the runner principal '${bakeRunnerPrincipal}' — the bake-runner authenticates by its credential (token-derived source), so a '<BAKE_RUNNER_TOKEN>=${bakeRunnerPrincipal}' SERVICE_AUTH_TOKENS entry is required or claim/ingest/heartbeat all 403 (IMAGE-API-DESIGN.md §P2.12 boot invariant)`);
449
- }
450
- const projectMemoryEnabled = boolEnvWithLegacyNegated("PROJECT_MEMORY_ENABLED", "PROJECT_MEMORY_DISABLED", true);
451
- configLkgEnabled();
452
- hostBackgroundShellEnabled();
453
- hostExecSpoolEnabled();
454
- const lspHostEnabled = (() => {
455
- const explicitHost = process.env.LSP_HOST_ENABLED;
456
- if (explicitHost === undefined || explicitHost === "") {
457
- if (process.env.LSP_ENABLED === "false") {
458
- CONFIG_NOTICES.push({
459
- event: "config_env_deprecated_lane_alias",
460
- fields: { deprecated: "LSP_ENABLED=false", replacement: "LSP_HOST_ENABLED=false", lane: "host", effective: "false" },
461
- });
462
- registerKnob({ name: "LSP_HOST_ENABLED", polarity: "opt-out", value: false, source: "legacy-env", legacyName: "LSP_ENABLED" });
463
- return false;
464
- }
465
- }
466
- return boolEnv("LSP_HOST_ENABLED", true);
467
- })();
468
666
  return {
469
- port: Number(env("PORT", "8090")),
470
667
  gatewayBaseUrl,
471
668
  gatewayApiKey: process.env.MODEL_API_KEY,
472
669
  gatewayFallbackUrls: csv("MODEL_GATEWAY_FALLBACK_URLS"),
670
+ // Anthropic-compatible-ecosystem env compat (2026-07-03): the route now assembles when EITHER credential
671
+ // is present — `ANTHROPIC_API_KEY` (x-api-key) or `ANTHROPIC_AUTH_TOKEN` (Authorization: Bearer; many such
672
+ // providers accept only this). AUTH_TOKEN wins when both are set. Base URL reads the widely-adopted
673
+ // standard name `ANTHROPIC_BASE_URL` too — the explicit sema name (no underscore) wins on a conflict.
674
+ // F8: alias selection via `firstSetEnv` ("" = unset) — the SAME helper the provider inference above
675
+ // consumes, so an empty first alias can neither suppress the route nor shadow the fallback base URL.
473
676
  anthropic: firstSetEnv("ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN")
474
677
  ? {
475
678
  apiKey: firstSetEnv("ANTHROPIC_API_KEY"),
@@ -481,8 +684,16 @@ export function loadConfig() {
481
684
  }
482
685
  : undefined,
483
686
  resilience: {
687
+ // BEHAVIOR CHANGE ([880] stall-watchdog audit): connect/firstToken default ON. core's idle
688
+ // timer only arms AFTER the first token (bumpIdle lives in sawContentToken), so with these
689
+ // two off a stream that opens but never emits hangs unbounded — the one true blind spot the
690
+ // fleet audit found. Values follow the [880] recommendation; =0 restores off.
484
691
  connectTimeoutMs: Number(env("MODEL_CONNECT_TIMEOUT_MS", "30000")),
485
692
  firstTokenTimeoutMs: Number(env("MODEL_FIRST_TOKEN_TIMEOUT_MS", "120000")),
693
+ // BEHAVIOR CHANGE (CC 196 parity): mid-stream stall watchdog default ON at 5min —
694
+ // after output starts, 5min of zero deltas cancels the stream as a retryable [network] failure
695
+ // (retry/failover recovers; without it a hung gateway pins the turn until the user Escapes).
696
+ // MODEL_IDLE_TIMEOUT_MS=0 restores off.
486
697
  idleTimeoutMs: Number(env("MODEL_IDLE_TIMEOUT_MS", "300000")),
487
698
  circuitBreaker: boolEnv("MODEL_CIRCUIT_BREAKER", false),
488
699
  failureThreshold: Number(env("MODEL_CB_FAILURE_THRESHOLD", "5")),
@@ -490,48 +701,279 @@ export function loadConfig() {
490
701
  },
491
702
  model,
492
703
  models,
493
- modelApiKeyEnv: {},
494
- modelApiKeys: {},
495
- modelQuotaWeights: {},
496
- tiers: {},
497
- projects: {},
704
+ modelApiKeyEnv: {}, // env path has a single gateway key; sema-registry populates per-model overrides
705
+ modelApiKeys: {}, // sealed-box 解封产物(内存-only 明文):env lane 恒空;applyEffective 解封填充
706
+ modelQuotaWeights: {}, // weight-at-burn 表:env lane 恒空(weight=1 兜底);registry apply 填充
707
+ tiers: {}, // 档位表:env lane 恒空(INERT);applyEffective 从 models.tierGroups 解析填充(保引用)
708
+ projects: {}, // 142-S4 项目登记簿:env lane 恒空(INERT);applyEffective 从 projects 域填充(保引用)
498
709
  roles,
499
- sessionBackend,
500
- sessionCacheTtlSec: Number(env("SESSION_CACHE_TTL_SEC", "300")),
501
- rewindSnapshotMaxMb: optFinitePositiveEnv("REWIND_SNAPSHOT_MAX_MB"),
710
+ cascadeLadder: csv("MODEL_CASCADE_LADDER"),
711
+ degrade: process.env.MODEL_DEGRADE_TO
712
+ ? {
713
+ to: env("MODEL_DEGRADE_TO"),
714
+ atCostFraction: Number(env("MODEL_DEGRADE_AT_COST_FRACTION", "0.7")),
715
+ reactive: boolEnv("MODEL_DEGRADE_REACTIVE", false),
716
+ downgradeOn: csv("MODEL_DEGRADE_ON").length
717
+ ? csv("MODEL_DEGRADE_ON")
718
+ : undefined,
719
+ // vision precheck (adversarial-review finding): the degrade TARGET is an external gateway
720
+ // model NOT in config.models, so resolveSpec's precheck (which only sees the picked model) can't see its
721
+ // vision capability — a vision-capable main + text-only degrade target would let images reach the text-only
722
+ // gateway as the opaque 400 the precheck prevents, AFTER admission, at runtime degrade. Declare it via
723
+ // MODEL_DEGRADE_TO_VISION (strict "true"). DEFAULT false = assume the cheaper degrade target is text-only
724
+ // (the common case, e.g. deepseek), so resolveSpec drops degrade for image-carrying tasks (main.ts).
725
+ toSupportsImages: boolEnv("MODEL_DEGRADE_TO_VISION", false),
726
+ }
727
+ : undefined,
728
+ };
729
+ }
730
+ /** 域:approval(审批/HITL 门)—— 审批名单与预算、durable 面、直连门开关与钥、敏感写门、elicitation。
731
+ * 注:直连门的**激活**(directDoorActive)与 D-G §3 半开门不变量是跨域合取,留在装配层。 */
732
+ function parseApprovalDomain(ctx) {
733
+ const { postureOn } = ctx; // 跨域入参②:posture 三态(single-user turnkey ⇒ HITL 面默认 ON)
734
+ // design/80 D-G: the direct-connect approval door anchors (all must be present to ACTIVATE; fail-closed).
735
+ const directApprovalDoor = boolEnv("DIRECT_APPROVAL_DOOR", false);
736
+ const dgHmacKeys = parseApprovalHmacKeys(process.env.APPROVAL_HMAC_KEYS);
737
+ // DURABLE_APPROVAL 既是本域字段,也是直连门的硬前置 —— 那条前置的理由与断言在装配层的跨域不变量段。
738
+ const durableApproval = boolEnv("DURABLE_APPROVAL", false);
739
+ return {
740
+ approvalRequire: csv("APPROVAL_REQUIRE"),
741
+ approvalDeny: csv("APPROVAL_DENY"),
742
+ approvalPollMs: Number(env("APPROVAL_POLL_MS", "1500")),
743
+ durableApproval,
744
+ resourceSuspend: boolEnv("RESOURCE_SUSPEND", false),
745
+ resourceSuspendTtlSec: Math.max(0, numEnv("RESOURCE_SUSPEND_TTL_SEC", "0")), // 0 ⇒ core default (30d)
746
+ approvalAutoBudget: Math.min(10000, Math.max(0, numEnv("APPROVAL_AUTO_BUDGET", "0"))), // clamp [0,10000]; 0 = off
747
+ approvalNeverAuto: csv("APPROVAL_NEVER_AUTO"),
748
+ approvalHmacKeys: dgHmacKeys, // D-G: WIRED (see the field's JSDoc); empty ⇒ inactive, not unimplemented
749
+ directApprovalDoor,
750
+ // ③ sensitive-path write deny set: unset = core's recommended set; explicit value = full replacement
751
+ // (comma-separated); "off"/empty = disabled. Set curation is core's (成文 core-side); server passes through.
752
+ sensitiveWritePatterns: ((raw) => {
753
+ if (raw === undefined)
754
+ return [...RECOMMENDED_SENSITIVE_PATTERNS];
755
+ const trimmed = raw.trim();
756
+ if (trimmed === "" || trimmed.toLowerCase() === "off")
757
+ return [];
758
+ return trimmed.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
759
+ })(process.env.SENSITIVE_WRITE_PATTERNS),
760
+ // [1557]§四: opt-in only — any value other than the two real gate shapes (incl. "off"/unset) stays undefined
761
+ // (no field on the config object at all), so the fs-write gate wiring never even considers it.
762
+ ...(process.env.MANUAL_MODE_SHELL_GATE === "always" || process.env.MANUAL_MODE_SHELL_GATE === "classify"
763
+ ? { manualModeShellGate: process.env.MANUAL_MODE_SHELL_GATE }
764
+ : {}),
765
+ mcpElicitation: {
766
+ enabled: boolEnv("MCP_ELICITATION_ENABLED", false), // E23 inbound elicitation; default OFF (fail-closed)
767
+ throttle: {
768
+ maxConcurrentPerRun: numEnvBounded("MCP_ELICITATION_MAX_CONCURRENT", String(DEFAULT_ELICITATION_THROTTLE.maxConcurrentPerRun), 1, 64),
769
+ maxTotalPerRun: numEnvBounded("MCP_ELICITATION_MAX_TOTAL", String(DEFAULT_ELICITATION_THROTTLE.maxTotalPerRun), 1, 10_000),
770
+ minIntervalMsPerServer: numEnvBounded("MCP_ELICITATION_MIN_INTERVAL_MS", String(DEFAULT_ELICITATION_THROTTLE.minIntervalMsPerServer), 0, 600_000),
771
+ ttlMs: numEnvBounded("MCP_ELICITATION_TTL_MS", String(DEFAULT_ELICITATION_THROTTLE.ttlMs), 1_000, 3_600_000),
772
+ },
773
+ },
774
+ askQuestionEnabled: postureOn("ASK_QUESTION_ENABLED"), // §4④ AskUserQuestion live HITL; posture-gated (single-user → ON)
775
+ toolApprovalEnabled: postureOn("TOOL_APPROVAL_ENABLED"), // [816]/[820]② live tool-approval HITL; posture-gated (single-user → ON), mirrors askQuestion
776
+ // [875]a 成文:0(缺省)= durable HITL 无限期等人,时间型 reapSuspended 不跑;file/memory lane 的回收
777
+ // 探针只在 DURABLE_APPROVAL=true 时注入(无 durable 的部署两只 suspended 回收器恒 NO-OP,parked 行的
778
+ // 恢复把手 = POST /v1/runs/:id/cancel,[868]①)。checkpoint 过期驱动的那只不受本旋钮门控。
779
+ approvalTimeoutSec: numEnv("APPROVAL_TIMEOUT_SEC", "0"),
780
+ };
781
+ }
782
+ /** 域:memory(记忆面 + TOC 同步腿)—— 记忆引擎开关/方言/根目录、scope 缺省、同步 client 三键、项目记忆。 */
783
+ function parseMemoryDomain(ctx) {
784
+ const { requirePrincipal } = ctx; // 跨域入参①:多租户下记忆面 DARK(scope 缺省不铸)
785
+ // design/138 S1 (clay 2026-07-08): long-term memory = the injection-first file-based memory ENGINE
786
+ // (core RunnerDeps.memoryBackend), default ON with MEMORY_ENGINE=off as the explicit kill-switch.
787
+ // The legacy MEMORY_BACKEND/EMBEDDING_* store plane was dropped without migration (those env vars are
788
+ // no longer read). Single-user gating lives in main.ts (the engine is never wired multi-tenant).
789
+ const memoryEngineEnabled = process.env.MEMORY_ENGINE !== "off";
790
+ // S3-TOB(边界重切后的 backend 选择,设计 §1.3):记忆持久面方言。缺省 file=现状零变化;
791
+ // pg/tidb=DB 真身(零卷主档)——多租户点亮的唯一门(显式 opt-in,复审 F5:绝不静默翻转)。
792
+ const memoryEngineBackendRaw = process.env.MEMORY_ENGINE_BACKEND ?? "file";
793
+ if (!["file", "pg", "tidb"].includes(memoryEngineBackendRaw)) {
794
+ throw new Error(`MEMORY_ENGINE_BACKEND must be file|pg|tidb, got "${memoryEngineBackendRaw}"`);
795
+ }
796
+ const memoryEngineBackend = memoryEngineBackendRaw;
797
+ // Single-user memory scope (hoisted from the config literal so the sync-scope default below can consume it):
798
+ // explicit MEMORY_SCOPE always wins; default "local" only when the engine is on AND single-user (see the
799
+ // literal-site comment on why multi-tenant/engine-off must stay undefined = memory dark).
800
+ const memoryScope = process.env.MEMORY_SCOPE ?? (memoryEngineEnabled && !requirePrincipal ? "local" : undefined);
801
+ // 142-S2.5-W1: TOC 同步 client 腿的三键(file memory 形态专用)。MEMORY_SYNC_URL 设了=开;
802
+ // 缺省 undefined=纯本地现状零变化。半配置 fail-loud(memoryEngineBackend 同款姿势,绝不静默降级):
803
+ // - URL 有 TOKEN 无 ⇒ throw(中心同步面要 Bearer,缺 token 的“开了但永远 401”是半配置陷阱);
804
+ // - TOKEN/SCOPE 有 URL 无 ⇒ throw(operator 显然想开同步,静默不跑=同款陷阱,对称拒启);
805
+ // - DB backend(pg/tidb)配了任意同步键 ⇒ throw(中心侧自己就是 POST /v1/memory/sync/:scope 的权威,
806
+ // 不该再跑 client 腿——两个半场叠在一个进程是拓扑错误)。
807
+ // scope 缺省 = formatUserScope(memoryScope)(中心侧 owner 门的 user 盘键铸法);memoryScope 本身已是
808
+ // v2 typed key(user:/org:/proj:/userproj:,只可能来自 operator 显式 MEMORY_SCOPE)则原样直通——
809
+ // 再包一层会铸出 `user:user%3A...` 双包键(诚实偏离,报告记档)。
810
+ const memorySyncUrl = process.env.MEMORY_SYNC_URL || undefined;
811
+ const memorySyncToken = process.env.MEMORY_SYNC_TOKEN || undefined;
812
+ const memorySyncScopeRaw = process.env.MEMORY_SYNC_SCOPE || undefined;
813
+ let memorySync;
814
+ if (memorySyncUrl === undefined) {
815
+ if (memorySyncToken !== undefined || memorySyncScopeRaw !== undefined) {
816
+ throw new Error("MEMORY_SYNC_TOKEN/MEMORY_SYNC_SCOPE are set but MEMORY_SYNC_URL is not — refusing to start half-configured (set MEMORY_SYNC_URL to enable memory sync, or unset the other MEMORY_SYNC_* keys)");
817
+ }
818
+ }
819
+ else {
820
+ if (memoryEngineBackend !== "file") {
821
+ throw new Error(`MEMORY_SYNC_URL is a FILE-memory (TOC client) knob, but MEMORY_ENGINE_BACKEND=${memoryEngineBackend} — the DB memory plane IS the central sync authority (it serves POST /v1/memory/sync/:scope) and must not also run the client leg; unset MEMORY_SYNC_* here`);
822
+ }
823
+ if (memorySyncToken === undefined) {
824
+ throw new Error("MEMORY_SYNC_URL is set but MEMORY_SYNC_TOKEN is not — refusing to start half-configured (the central sync face requires a bearer token)");
825
+ }
826
+ const isV2ScopeKey = (k) => k.startsWith("user:") || k.startsWith("org:") || k.startsWith("proj:") || k.startsWith("userproj:");
827
+ const scope = memorySyncScopeRaw ?? (memoryScope !== undefined ? (isV2ScopeKey(memoryScope) ? memoryScope : formatUserScope(memoryScope)) : undefined);
828
+ if (scope === undefined) {
829
+ throw new Error("MEMORY_SYNC_URL is set but no sync scope is derivable (memory engine off or multi-tenant without an explicit scope) — set MEMORY_SYNC_SCOPE, or run the single-user file memory engine so MEMORY_SCOPE (default \"local\") provides one");
830
+ }
831
+ memorySync = { url: memorySyncUrl, token: memorySyncToken, scope, ...((v) => (v !== undefined && Number.isInteger(v) && v >= 1 ? { maxPushEntries: v } : {}))(process.env.MEMORY_SYNC_MAX_PUSH_ENTRIES !== undefined ? Number(process.env.MEMORY_SYNC_MAX_PUSH_ENTRIES) : undefined), ...((v) => (v !== undefined && Number.isInteger(v) && v >= 1 ? { maxPullEntries: v } : {}))(process.env.MEMORY_SYNC_MAX_PULL_ENTRIES !== undefined ? Number(process.env.MEMORY_SYNC_MAX_PULL_ENTRIES) : undefined) };
832
+ }
833
+ // design/158 B4 ② 的 D 家族四枚里,唯一落在 ServiceConfig 上的就是这枚(其余三枚只在使用点懒读,
834
+ // 预登记留在 orchestration 域 —— 顺序与改前一致:本枚的弃用通知仍先于那三枚)。
835
+ const projectMemoryEnabled = boolEnvWithLegacyNegated("PROJECT_MEMORY_ENABLED", "PROJECT_MEMORY_DISABLED", true);
836
+ return {
502
837
  memoryEngineEnabled,
503
838
  memoryEngineDir: process.env.MEMORY_ENGINE_DIR || undefined,
504
- memoryEngineRemoteLaneAllowed: process.env.MEMORY_ENGINE_REMOTE_LANE === "allow",
839
+ memoryEngineRemoteLaneAllowed: process.env.MEMORY_ENGINE_REMOTE_LANE === "allow", // N0: fail-closed default
840
+ // A single-user deployment has no principal, so `memoryScopeFor` falls back to `config.memoryScope` — and if
841
+ // THAT were unset the resolved scope would be undefined ⇒ resolveSpec builds NO `spec.memory` ⇒ the memory
842
+ // engine never materializes (the "wired-not-triggered" gap a §4 real test hit on the legacy plane).
843
+ // Default it to "local" for single-user (REQUIRE_PRINCIPAL !== "true") so TOC local memory works out-of-the-box
844
+ // (CC-parity). Multi-tenant memory is DARK by design (design/138 S1: the file basement has no tenant isolation —
845
+ // memoryScopeFor returns undefined there). An explicit MEMORY_SCOPE always wins.
505
846
  memoryEngineBackend,
506
- ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}),
507
- memoryScope,
847
+ memoryScope, // hoisted above (the 142-S2.5-W1 sync-scope default consumes it)
508
848
  ...(memorySync ? { memorySync } : {}),
849
+ projectMemoryEnabled, // design/113 C4 opt-out, flipped positive in design/158 B4 (legacy PROJECT_MEMORY_DISABLED still honored)
850
+ syncImportLeaseStaleSec: Math.max(0, numEnv("SYNC_IMPORT_LEASE_STALE_SEC", "600")),
851
+ };
852
+ }
853
+ /** 域:auth(身份/凭证/绑址)—— service token 面、CORS、principal 头与开关、JWT 锚、autonomy、绑址。 */
854
+ function parseAuthDomain(ctx) {
855
+ const { requirePrincipal } = ctx; // 装配层已解析(它是四个域的共同入参),本域只负责把它落到字段上
856
+ // design/80 D-G 直连门的 principal-JWT 锚:验签材料属 auth 域,门的激活与半开门不变量在装配层。
857
+ const principalJwtPubkeys = parsePrincipalJwks(process.env.PRINCIPAL_JWT_PUBKEYS);
858
+ const principalJwtIss = process.env.PRINCIPAL_JWT_ISS || undefined;
859
+ const principalJwtAud = process.env.PRINCIPAL_JWT_AUD || undefined;
860
+ const principalJwtMaxTtlSec = Math.max(0, numEnv("PRINCIPAL_JWT_MAX_TTL_SEC", "120")); // 0 = no cap; default ≤120s
861
+ // Per-system service credentials (token → system name). Parsed as a DOMAIN field (not inline in the return
862
+ // literal) so the bake boot invariant at the assembly layer can assert the runner-credential→runnerPrincipal
863
+ // mapping exists.
864
+ const authTokens = Object.fromEntries((process.env.SERVICE_AUTH_TOKENS ?? "")
865
+ .split(",")
866
+ .map((s) => s.trim())
867
+ .filter((s) => s.includes("="))
868
+ .map((s) => [s.slice(0, s.indexOf("=")).trim(), s.slice(s.indexOf("=") + 1).trim()])
869
+ .filter(([tok, sys]) => tok.length > 0 && sys.length > 0));
870
+ return {
871
+ // 绑址([1934]):BIND_HOST 优先,HOST 兼容(cli/桌面传的就是它);空串=未设。
872
+ bindHost: process.env.BIND_HOST || process.env.HOST || undefined,
873
+ authToken: process.env.SERVICE_AUTH_TOKEN,
874
+ authTokens,
875
+ allowUnauthedWrites: boolEnv("ALLOW_UNAUTHED_WRITES", false),
876
+ corsOrigins: (process.env.CORS_ORIGIN ?? "").split(",").map((s) => s.trim()).filter(Boolean),
877
+ principalHeader: headerNameEnv("PRINCIPAL_HEADER", "x-agent-principal"),
878
+ requirePrincipal,
879
+ // Runtime governance second baton (center §10): AUTONOMY env for the env/local path; sema-registry overlays
880
+ // it (applyRuntimeHot). commandPolicy is structured config — sema-registry-only (no flat env scalar form).
881
+ autonomy: parseAutonomy(process.env.AUTONOMY),
882
+ operatorPrincipals: csv("OPERATOR_PRINCIPALS"),
883
+ principalJwtPubkeys,
884
+ principalJwtIss,
885
+ principalJwtAud,
886
+ principalJwtMaxTtlSec,
887
+ };
888
+ }
889
+ /** 域:orchestration(编排 + 执行车道)—— remoteExec 五车道/worktree 隔离、leader/router/scheduler/fork、
890
+ * workflow 与后台 agent 的存留旋钮、沙箱面(pkg 源/env facts/LSP)、烤镜像门。 */
891
+ function parseOrchestrationDomain(ctx) {
892
+ const { postureOn } = ctx; // 跨域入参②:posture 三态(single-user turnkey ⇒ 自编排/fork/scheduler 默认 ON)
893
+ // workflowSizeGuideline: CC normalizes unknown→unrestricted (lenient), but an env knob typo
894
+ // silently degrading to "no guidance" is the half-config trap — fail-loud here (memoryEngineBackend
895
+ // posture; the ADVISORY nature is core's concern, the env spelling is ours).
896
+ const workflowSizeGuidelineRaw = process.env.WORKFLOW_SIZE_GUIDELINE;
897
+ if (workflowSizeGuidelineRaw !== undefined && !["small", "medium", "large", "unrestricted"].includes(workflowSizeGuidelineRaw)) {
898
+ throw new Error(`WORKFLOW_SIZE_GUIDELINE must be small|medium|large|unrestricted, got "${workflowSizeGuidelineRaw}"`);
899
+ }
900
+ const workflowSizeGuideline = workflowSizeGuidelineRaw;
901
+ const bakeRunnerPrincipal = env("BAKE_RUNNER_PRINCIPAL", "system:bake-runner");
902
+ // ── design/158 B4 ② — the four knobs whose canonical name went from negative to positive. Only the first
903
+ // lands on `ServiceConfig`; the other three are read lazily at their use sites (main.ts's LKG gate,
904
+ // remote-env-host's capability probe and exec path) but are resolved HERE too so that their polarity row and
905
+ // any legacy-alias deprecation notice are emitted once, at boot, with every other config diagnostic — an
906
+ // operator must not have to trigger a background shell before being told the name they set is retired.
907
+ // (家族的第一枚 PROJECT_MEMORY_ENABLED 是 memory 域的字段,已在那里解析 —— 通知顺序不变。)
908
+ configLkgEnabled();
909
+ hostBackgroundShellEnabled();
910
+ hostExecSpoolEnabled();
911
+ // ── design/158 B4 ③ — LSP: one knob per lane. `LSP_ENABLED` used to drive BOTH the sandbox lane (opt-in,
912
+ // default OFF — it needs a baked `sema-code-lsp` template) and the host lane (opt-out, default ON — it needs
913
+ // nothing and degrades to grep/read), i.e. one env name with two OPPOSITE defaults and no way to silence the
914
+ // host lane without also naming the sandbox knob. `LSP_HOST_ENABLED` now owns the host lane; an explicit
915
+ // `LSP_ENABLED=false` keeps working as a host opt-out (it was the only one operators ever had) with a notice
916
+ // naming the replacement, and `LSP_HOST_ENABLED` always wins when set.
917
+ const lspHostEnabled = (() => {
918
+ const explicitHost = process.env.LSP_HOST_ENABLED;
919
+ if (explicitHost === undefined || explicitHost === "") {
920
+ if (process.env.LSP_ENABLED === "false") {
921
+ CONFIG_NOTICES.push({
922
+ event: "config_env_deprecated_lane_alias",
923
+ fields: { deprecated: "LSP_ENABLED=false", replacement: "LSP_HOST_ENABLED=false", lane: "host", effective: "false" },
924
+ });
925
+ registerKnob({ name: "LSP_HOST_ENABLED", polarity: "opt-out", value: false, source: "legacy-env", legacyName: "LSP_ENABLED" });
926
+ return false;
927
+ }
928
+ }
929
+ return boolEnv("LSP_HOST_ENABLED", true);
930
+ })();
931
+ return {
509
932
  remoteExec: process.env.REMOTE_EXEC === "e2b" && process.env.E2B_API_KEY
510
933
  ? {
511
934
  provider: "e2b",
512
935
  apiKey: process.env.E2B_API_KEY,
513
936
  ...(process.env.E2B_TEMPLATE ? { template: process.env.E2B_TEMPLATE } : {}),
514
937
  ...(process.env.E2B_TIMEOUT_MS ? { timeoutMs: Number(process.env.E2B_TIMEOUT_MS) } : {}),
938
+ // Idle/liveness bound for a command (default 120s, the hang-fix). A long SILENT build —
939
+ // gradle's first dependency download produces no output for minutes (gate#2 P1 finding) — looks
940
+ // like a hang and gets killed; raise this for workers that run such tools. Agents should still
941
+ // prefer background+poll, but this is the operator escape hatch.
515
942
  ...(process.env.E2B_LIVENESS_MS ? { livenessMs: Number(process.env.E2B_LIVENESS_MS) } : {}),
516
943
  ...(process.env.E2B_ALLOW_NET ? { allowInternetAccess: boolEnv("E2B_ALLOW_NET", true) } : {}),
944
+ // Out-of-band secret/config env injected sandbox-wide (OA Phase-B, design/48 §5 gap 7):
945
+ // E2B_SANDBOX_ENV is a CSV of THIS worker's OWN env-var NAMES to forward into the sandbox under the
946
+ // same name (name-only config keeps the secret in the worker's env, never in the spec/center). e.g.
947
+ // `E2B_SANDBOX_ENV=GIT_TOKEN` → the agent's git can auth via $GIT_TOKEN (GIT_ASKPASS / credential
948
+ // helper) without the token ever entering the prompt, the command string, or the durable event log.
517
949
  ...(process.env.E2B_SANDBOX_ENV ? { sandboxEnv: resolveEnvForward(process.env.E2B_SANDBOX_ENV) } : {}),
950
+ // [1452]/[1454] CWD-A:workspace 根显式可配(其余四车道 *_MOUNT_PATH 早有;e2b 此前配置层
951
+ // 不可达,恒 /home/user)。exec/spawnBackground/fs 三腿同源消费(remote-env-e2b this.cwd)。
952
+ // ⚠️ 变更纪律(codex 终审 high):resume 契约只回传 snapshotId,恢复的沙箱以**当前**配置值为
953
+ // cwd——改此值前先 drain 挂起中的 e2b 任务,否则 resumed 任务的 exec 落新根而产物在旧根
954
+ // (根修=core resumeVM 契约带回 WorkspaceHandle.mountPath,已转 core)。
518
955
  ...(process.env.E2B_MOUNT_PATH ? { mountPath: process.env.E2B_MOUNT_PATH } : {}),
519
956
  }
520
- :
957
+ : // Kata pod sandbox on our k3s: image required; auth explicit (K8S_API_URL/K8S_TOKEN) or in-cluster SA.
521
958
  process.env.REMOTE_EXEC === "k8s" && process.env.K8S_SANDBOX_IMAGE
522
959
  ? {
523
960
  provider: "k8s",
524
961
  image: process.env.K8S_SANDBOX_IMAGE,
525
962
  ...(process.env.K8S_API_URL ? { apiUrl: process.env.K8S_API_URL } : {}),
526
963
  ...(process.env.K8S_TOKEN ? { token: process.env.K8S_TOKEN } : {}),
964
+ // base64 to survive sema-registry's no-newline secrets API (same constraint as SSH_PRIVATE_KEY).
527
965
  ...(process.env.K8S_CA_CERT_B64 ? { caCert: Buffer.from(process.env.K8S_CA_CERT_B64, "base64").toString("utf8") } : {}),
528
966
  ...(boolEnv("K8S_INSECURE_TLS", false) ? { insecureTls: true } : {}),
529
967
  ...(process.env.K8S_NAMESPACE ? { namespace: process.env.K8S_NAMESPACE } : {}),
530
968
  ...(process.env.K8S_RUNTIME_CLASS != null ? { runtimeClass: process.env.K8S_RUNTIME_CLASS } : {}),
531
969
  ...(process.env.K8S_MOUNT_PATH ? { mountPath: process.env.K8S_MOUNT_PATH } : {}),
532
970
  ...(process.env.K8S_TIMEOUT_MS ? { timeoutMs: Number(process.env.K8S_TIMEOUT_MS) } : {}),
971
+ // Resource profile knob (resources are profile data, not mechanism — center/service
972
+ // config, never wire-hardcoded). Found live: 25-module gradle integration OOM'd the 2Gi default.
533
973
  ...(process.env.K8S_MEMORY ? { memory: process.env.K8S_MEMORY } : {}),
534
974
  ...(process.env.K8S_CPU ? { cpu: process.env.K8S_CPU } : {}),
975
+ // Workspace-durable suspend/resume: adapter-held MinIO creds (secret minio-agent-worker);
976
+ // the sandbox only ever sees a presigned URL. All three keys required to enable — else suspendable:false.
535
977
  ...(process.env.MINIO_ENDPOINT && process.env.MINIO_ACCESS_KEY && process.env.MINIO_SECRET_KEY
536
978
  ? {
537
979
  s3Snapshot: {
@@ -546,7 +988,7 @@ export function loadConfig() {
546
988
  }
547
989
  : {}),
548
990
  }
549
- :
991
+ : // design/61: SSH → a real host. Key from the adapter-held secret env SSH_PRIVATE_KEY (never the workspace).
550
992
  process.env.REMOTE_EXEC === "ssh" && process.env.SSH_HOST && process.env.SSH_USER && process.env.SSH_PRIVATE_KEY
551
993
  ? {
552
994
  provider: "ssh",
@@ -556,7 +998,7 @@ export function loadConfig() {
556
998
  ...(process.env.SSH_PORT ? { port: Number(process.env.SSH_PORT) } : {}),
557
999
  ...(process.env.SSH_MOUNT_PATH ? { mountPath: process.env.SSH_MOUNT_PATH } : {}),
558
1000
  }
559
- :
1001
+ : // design/61: ADB → a real device (serial or host:port). adb binary must be on PATH in the image.
560
1002
  process.env.REMOTE_EXEC === "adb" && process.env.ADB_SERIAL
561
1003
  ? {
562
1004
  provider: "adb",
@@ -564,14 +1006,18 @@ export function loadConfig() {
564
1006
  ...(process.env.ADB_PATH ? { adbPath: process.env.ADB_PATH } : {}),
565
1007
  ...(process.env.ADB_MOUNT_PATH ? { mountPath: process.env.ADB_MOUNT_PATH } : {}),
566
1008
  }
567
- :
1009
+ : // DUAL-MODE §5: `host` = run on THIS machine, no container (the TOC default). No required env.
568
1010
  process.env.REMOTE_EXEC === "host"
569
1011
  ? {
570
1012
  provider: "host",
571
1013
  ...(process.env.HOST_WORKSPACE_BASE ? { workspaceBase: process.env.HOST_WORKSPACE_BASE } : {}),
572
1014
  ...(process.env.HOST_COMMAND_TIMEOUT_MS ? { commandTimeoutMs: Number(process.env.HOST_COMMAND_TIMEOUT_MS) } : {}),
573
1015
  }
574
- :
1016
+ : // DUAL-MODE §5: `local-docker` = a per-task container on THIS machine's docker daemon (isolation:true,
1017
+ // suspendable:false). DOCKER_IMAGE is REQUIRED — no docker.io default (domestic-images iron rule); a
1018
+ // deployment points it at the docker.io/claybobby public pool or an in-network mirror (SWR
1019
+ // deprecated). Secrets are env-NAMEs the worker resolves from its
1020
+ // own process.env (DOCKER_SANDBOX_ENV, a CSV of NAMEs) — never the values in the spec/center.
575
1021
  process.env.REMOTE_EXEC === "local-docker" && process.env.DOCKER_IMAGE
576
1022
  ? {
577
1023
  provider: "local-docker",
@@ -589,10 +1035,17 @@ export function loadConfig() {
589
1035
  ...(process.env.DOCKER_COMMAND_TIMEOUT_MS ? { commandTimeoutMs: Number(process.env.DOCKER_COMMAND_TIMEOUT_MS) } : {}),
590
1036
  ...(process.env.DOCKER_SANDBOX_ENV ? { env: resolveEnvForward(process.env.DOCKER_SANDBOX_ENV) } : {}),
591
1037
  }
592
- :
1038
+ : // TOC first-run default: in LOCAL config mode (CONFIG_PROVIDER=local — the
1039
+ // single-machine source) with no explicit REMOTE_EXEC, default to the `host` lane so "fill .env
1040
+ // and run" works without the user setting REMOTE_EXEC. Fleet/remote (CONFIG_PROVIDER≠local) keeps
1041
+ // the in-process stub default (REMOTE_EXEC unset = OA/review posture) — unchanged.
593
1042
  process.env.CONFIG_PROVIDER === "local"
594
1043
  ? { provider: "host" }
595
1044
  : undefined,
1045
+ // SVC-3 git-worktree isolation: opt-in, operator-supplied ONLY. WORKTREE_REPO_ROOT is the trusted base
1046
+ // repo; WORKTREE_ALLOWED_ROOTS (CSV) is the defence-in-depth allowlist (default = [repoRoot]);
1047
+ // WORKTREE_BASE_COMMIT pins the worktree ref (default core HEAD). Enabled only when both the flag and the
1048
+ // repo root are present — a half-set config stays OFF (no silent default repo).
596
1049
  worktreeIsolation: boolEnv("WORKTREE_ISOLATION_ENABLED", false) && process.env.WORKTREE_REPO_ROOT
597
1050
  ? {
598
1051
  repoRoot: process.env.WORKTREE_REPO_ROOT,
@@ -602,158 +1055,42 @@ export function loadConfig() {
602
1055
  ...(process.env.WORKTREE_BASE_COMMIT ? { commit: process.env.WORKTREE_BASE_COMMIT } : {}),
603
1056
  }
604
1057
  : undefined,
605
- dbBackend,
606
- dbBackendExplicit: dbBackendSet,
607
- localDataRoot,
608
- tidb: needsDb && dbBackend === "mysql"
609
- ? {
610
- host: env2("MYSQL_HOST", "TIDB_HOST"),
611
- port: Number(env2("MYSQL_PORT", "TIDB_PORT", process.env.MYSQL_HOST ? "3306" : "4000")),
612
- user: env2("MYSQL_USER", "TIDB_USER"),
613
- password: env2("MYSQL_PASSWORD", "TIDB_PASSWORD", ""),
614
- database: env2("MYSQL_DATABASE", "TIDB_DATABASE"),
615
- connectionLimit: process.env.MYSQL_POOL_SIZE || process.env.TIDB_POOL_SIZE
616
- ? Number(process.env.MYSQL_POOL_SIZE || process.env.TIDB_POOL_SIZE)
617
- : undefined,
618
- }
619
- : undefined,
620
- pg: needsDb && dbBackend === "pg"
621
- ? {
622
- host: env("PG_HOST"),
623
- port: Number(env("PG_PORT", "5432")),
624
- user: env("PG_USER"),
625
- password: env("PG_PASSWORD", ""),
626
- database: env("PG_DATABASE"),
627
- connectionLimit: process.env.PG_POOL_SIZE ? Number(process.env.PG_POOL_SIZE) : undefined,
628
- }
629
- : undefined,
630
- dbQueryTimeoutMs: optFinitePositiveEnv("DB_QUERY_TIMEOUT_MS"),
631
- snapshotBlobStore: process.env.MINIO_ENDPOINT && process.env.MINIO_ACCESS_KEY && process.env.MINIO_SECRET_KEY
632
- ? {
633
- endpoint: process.env.MINIO_ENDPOINT,
634
- bucket: process.env.SESSION_SNAPSHOT_BUCKET ?? "session-snapshots",
635
- accessKey: process.env.MINIO_ACCESS_KEY,
636
- secretKey: process.env.MINIO_SECRET_KEY,
637
- ...(process.env.MINIO_REGION ? { region: process.env.MINIO_REGION } : {}),
638
- ...(process.env.SESSION_SNAPSHOT_PREFIX ? { keyPrefix: process.env.SESSION_SNAPSHOT_PREFIX } : {}),
639
- ...((ttl) => (ttl !== undefined ? { presignTtlSec: ttl } : {}))(optFinitePositiveEnv("SESSION_SNAPSHOT_TTL_SEC")),
640
- }
641
- : undefined,
642
- snapshotBlobSqlMaxBytes: optFinitePositiveEnv("SNAPSHOT_BLOB_SQL_MAX_BYTES"),
643
- snapshotBlobAllowSql: boolEnv("SNAPSHOT_BLOB_ALLOW_SQL_BYTES", false),
644
- pluginsAllowHosts: (process.env.PLUGINS_ALLOW_HOSTS ?? "github.com").split(",").map((h) => h.trim()).filter(Boolean),
645
- bindHost: process.env.BIND_HOST || process.env.HOST || undefined,
646
- attachmentOrphanGraceMs: ((v) => (v !== undefined && Number.isFinite(v) && v >= 0 ? v : 3_600_000))(process.env.ATTACHMENT_ORPHAN_GRACE_MS !== undefined ? Number(process.env.ATTACHMENT_ORPHAN_GRACE_MS) : undefined),
647
- workspaceFileMaxBytes: optFinitePositiveEnv("WORKSPACE_FILE_MAX_BYTES") ?? 8 * 1024 * 1024,
648
- sendUserFile: (process.env.MINIO_ENDPOINT || process.env.S3_ENDPOINT) && process.env.MINIO_ACCESS_KEY && process.env.MINIO_SECRET_KEY
649
- ? {
650
- endpoint: (process.env.MINIO_ENDPOINT || process.env.S3_ENDPOINT),
651
- ...(process.env.S3_PUBLIC_ENDPOINT || process.env.S3_ENDPOINT
652
- ? { publicEndpoint: (process.env.S3_PUBLIC_ENDPOINT || process.env.S3_ENDPOINT) }
653
- : {}),
654
- publicBucket: process.env.S3_PUBLIC_BUCKET ?? "sema-public",
655
- privateBucket: process.env.SESSION_SNAPSHOT_BUCKET ?? "session-snapshots",
656
- privateKeyPrefix: "sendfile/",
657
- ...(process.env.SEND_USER_FILE_SANDBOX_PUT_ENDPOINT ? { sandboxPutEndpoint: process.env.SEND_USER_FILE_SANDBOX_PUT_ENDPOINT } : {}),
658
- accessKey: process.env.MINIO_ACCESS_KEY,
659
- secretKey: process.env.MINIO_SECRET_KEY,
660
- ...(process.env.MINIO_REGION ? { region: process.env.MINIO_REGION } : {}),
661
- defaultTtlSec: ((v) => (v !== undefined && Number.isInteger(v) && v >= 0 ? v : 0))(process.env.SEND_USER_FILE_URL_TTL !== undefined ? Number(process.env.SEND_USER_FILE_URL_TTL) : undefined),
662
- }
663
- : undefined,
664
- authToken: process.env.SERVICE_AUTH_TOKEN,
665
- authTokens,
666
- allowUnauthedWrites: boolEnv("ALLOW_UNAUTHED_WRITES", false),
667
- metricsToken: process.env.METRICS_TOKEN || undefined,
668
- traceToken: process.env.TRACE_TOKEN || undefined,
669
- corsOrigins: (process.env.CORS_ORIGIN ?? "").split(",").map((s) => s.trim()).filter(Boolean),
670
- attachmentMaxBytes: Math.max(1024, numEnv("ATTACHMENT_MAX_BYTES", String(32 * 1024 * 1024))),
671
- ...(process.env.ATTACHMENT_MIME_ALLOWLIST
672
- ? { attachmentMimeAllowlist: process.env.ATTACHMENT_MIME_ALLOWLIST.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean) }
673
- : {}),
674
- attachmentUnboundTtlMs: Math.max(60_000, numEnv("ATTACHMENT_UNBOUND_TTL_MS", String(24 * 3600 * 1000))),
675
- principalHeader: headerNameEnv("PRINCIPAL_HEADER", "x-agent-principal"),
676
- requirePrincipal,
677
- autonomy: parseAutonomy(process.env.AUTONOMY),
678
- approvalRequire: csv("APPROVAL_REQUIRE"),
679
- operatorPrincipals: csv("OPERATOR_PRINCIPALS"),
680
- cascadeLadder: csv("MODEL_CASCADE_LADDER"),
681
- approvalDeny: csv("APPROVAL_DENY"),
682
- approvalPollMs: Number(env("APPROVAL_POLL_MS", "1500")),
683
- durableApproval: boolEnv("DURABLE_APPROVAL", false),
684
- resourceSuspend: boolEnv("RESOURCE_SUSPEND", false),
685
- resourceSuspendTtlSec: Math.max(0, numEnv("RESOURCE_SUSPEND_TTL_SEC", "0")),
686
- approvalAutoBudget: Math.min(10000, Math.max(0, numEnv("APPROVAL_AUTO_BUDGET", "0"))),
687
- approvalNeverAuto: csv("APPROVAL_NEVER_AUTO"),
688
- approvalHmacKeys: dgHmacKeys,
689
- directApprovalDoor,
690
- principalJwtPubkeys,
691
- principalJwtIss,
692
- principalJwtAud,
693
- principalJwtMaxTtlSec,
694
- directDoorActive,
695
- infraCostRates: {
696
- toolCallMicroUsd: Math.max(0, numEnv("INFRA_COST_TOOL_CALL", "0")),
697
- sandboxSecMicroUsd: Math.max(0, numEnv("INFRA_COST_SANDBOX_SEC", "0")),
698
- egressGbMicroUsd: Math.max(0, numEnv("INFRA_COST_EGRESS_GB", "0")),
699
- },
700
1058
  leaderEnabled: boolEnv("LEADER_ENABLED", false),
701
1059
  leaderFanoutEnabled: boolEnv("LEADER_FANOUT_ENABLED", true),
702
- routerEnabled: boolEnv("ROUTER_ENABLED", false),
703
- selfOrchestrationEnabled: postureOn("SELF_ORCHESTRATION_ENABLED"),
704
- forkEnabled: postureOn("FORK_ENABLED"),
705
- experimentalObserverAgents: boolEnv("EXPERIMENTAL_OBSERVER_AGENTS", false),
706
- schedulerEnabled: postureOn("SCHEDULER_ENABLED"),
707
- projectMemoryEnabled,
708
- planModeEnabled: boolEnv("PLAN_MODE_ENABLED", true),
1060
+ routerEnabled: boolEnv("ROUTER_ENABLED", false), // A value router; default OFF (ship dark, enable after live validation)
1061
+ selfOrchestrationEnabled: postureOn("SELF_ORCHESTRATION_ENABLED"), // posture: single-user turnkey → ON (CC local trust; sandboxed vm); multi-tenant opt-in (RCE-ish, gated)
1062
+ forkEnabled: postureOn("FORK_ENABLED"), // §4 CC /fork posture: single-user turnkey → default-ON for the LLM; multi-tenant honored only with a per-principal entitlement resolver (core enforces allowFork, registry-core 0.1.47; enableForkFromBody)
1063
+ experimentalObserverAgents: boolEnv("EXPERIMENTAL_OBSERVER_AGENTS", false), // observer 开闸线:显式 "true" 才开(实验面,core ships dark——非 postureOn);多租户只认 center caps,env 被忽略(applyObserverEnvOptIn boot warn)
1064
+ schedulerEnabled: postureOn("SCHEDULER_ENABLED"), // R7 自唤醒 posture: single-user turnkey → ON (CC self-wake); multi-tenant routes to center (opt-in)
1065
+ planModeEnabled: boolEnv("PLAN_MODE_ENABLED", true), // EnterPlanMode (core 1.167) model-driven plan; 🆕 default ON (resident — clay), PLAN_MODE_ENABLED=false opts out
709
1066
  ...(process.env.SCHEDULER_STORE_PATH ? { schedulerStorePath: process.env.SCHEDULER_STORE_PATH } : {}),
710
- configBootFetchBudgetMs: Math.max(0, numEnv("CONFIG_BOOT_FETCH_BUDGET_MS", "1500")),
711
- schedulerSessionWakeup: boolEnv("SCHEDULER_SESSION_WAKEUP", true),
1067
+ schedulerSessionWakeup: boolEnv("SCHEDULER_SESSION_WAKEUP", true), // [1009]② host-signal: default ON, the spawning shell opts a daemon-less engine out (PLAN_MODE_ENABLED 同形解析)
712
1068
  selfOrchestrationModels: csv("SELF_ORCHESTRATION_MODELS"),
713
1069
  selfOrchestrationWorkerIsolation: boolEnv("SELF_ORCHESTRATION_WORKER_ISOLATION", false),
714
- workflowRunStoreBackend: enumEnv("WORKFLOW_RUN_STORE", "auto", ["auto", "file", "memory"]),
715
- workflowOrphanGraceMs: Math.max(60_000, numEnv("WORKFLOW_ORPHAN_GRACE_MS", String(24 * 60 * 60 * 1000))),
716
- workflowJournalRetentionMs: Math.max(60_000, numEnv("WORKFLOW_JOURNAL_RETENTION_MS", String(7 * 24 * 60 * 60 * 1000))),
717
- workflowRunRetentionMs: Math.max(60_000, numEnv("WORKFLOW_RUN_RETENTION_MS", String(30 * 24 * 60 * 60 * 1000))),
718
- backgroundAgentRetentionMs: Math.max(60_000, numEnv("BG_AGENT_RETENTION_MS", String(7 * 24 * 60 * 60 * 1000))),
719
- backgroundAgentStaleRunningMs: Math.max(10 * 60_000, numEnv("BG_AGENT_STALE_RUNNING_MS", String(15 * 60_000))),
720
- backgroundAgentParkClaimStaleMs: Math.max(60_000, numEnv("BG_AGENT_PARK_CLAIM_STALE_MS", String(60 * 60_000))),
721
- rosterRetentionMs: Math.max(60_000, numEnv("ROSTER_RETENTION_MS", String(30 * 24 * 60 * 60 * 1000))),
1070
+ ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}),
1071
+ workflowRunStoreBackend: enumEnv("WORKFLOW_RUN_STORE", "auto", ["auto", "file", "memory"]), // SVC-1 + P1: auto = SQL backend when present (cross-replica), else File
1072
+ workflowOrphanGraceMs: Math.max(60_000, numEnv("WORKFLOW_ORPHAN_GRACE_MS", String(24 * 60 * 60 * 1000))), // SVC-1: 24h default, floor 1m
1073
+ workflowJournalRetentionMs: Math.max(60_000, numEnv("WORKFLOW_JOURNAL_RETENTION_MS", String(7 * 24 * 60 * 60 * 1000))), // SVC-2: 7d default, floor 1m
1074
+ workflowRunRetentionMs: Math.max(60_000, numEnv("WORKFLOW_RUN_RETENTION_MS", String(30 * 24 * 60 * 60 * 1000))), // 30d default, floor 1m; SQL twins only
1075
+ backgroundAgentRetentionMs: Math.max(60_000, numEnv("BG_AGENT_RETENTION_MS", String(7 * 24 * 60 * 60 * 1000))), // core 1.364: 7d default, floor 1m
1076
+ backgroundAgentStaleRunningMs: Math.max(10 * 60_000, numEnv("BG_AGENT_STALE_RUNNING_MS", String(15 * 60_000))), // core 契约 ≥10min 硬钳(60s 写者心跳)
1077
+ backgroundAgentParkClaimStaleMs: Math.max(60_000, numEnv("BG_AGENT_PARK_CLAIM_STALE_MS", String(60 * 60_000))), // [1575] F1 修:default 1h,floor 1m(零/负值都不该让整块清算失活)
1078
+ rosterRetentionMs: Math.max(60_000, numEnv("ROSTER_RETENTION_MS", String(30 * 24 * 60 * 60 * 1000))), // [1522] MED2: 30d default, floor 1m; SQL twins only
1079
+ // 修8: scratchpad sweep TTL — 0(or negative)= disabled; enabled values floor at 1m (a sub-minute TTL would
1080
+ // race in-flight tasks' scratchpad writes for no operational gain).
722
1081
  scratchpadSweepTtlMs: ((n) => (n <= 0 ? 0 : Math.max(60_000, n)))(numEnv("SCRATCHPAD_SWEEP_TTL_MS", String(7 * 24 * 60 * 60 * 1000))),
1082
+ // RFC B5 region package-source selector — 🆕 default `global` (clay 拍 2026-07-13 海外为准:官方源 env-first
1083
+ // 对存量镜像即生效);国内部署显式 SANDBOX_PKG_SOURCE=cn;`none` = 完全不注入(旧 unset 行为,字节级不变)。
723
1084
  ...(process.env.SANDBOX_PKG_SOURCE !== "none"
724
1085
  ? { sandboxPkgSource: process.env.SANDBOX_PKG_SOURCE || "global" }
725
1086
  : {}),
726
- sessionAutoTitle: boolEnv("SESSION_AUTO_TITLE", true),
727
- selectEnvironmentTool: boolEnv("SELECT_ENVIRONMENT_TOOL", true),
728
- envFactsEnabled: boolEnv("SANDBOX_ENV_FACTS", true),
729
- sensitiveWritePatterns: ((raw) => {
730
- if (raw === undefined)
731
- return [...RECOMMENDED_SENSITIVE_PATTERNS];
732
- const trimmed = raw.trim();
733
- if (trimmed === "" || trimmed.toLowerCase() === "off")
734
- return [];
735
- return trimmed.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
736
- })(process.env.SENSITIVE_WRITE_PATTERNS),
737
- ...(process.env.MANUAL_MODE_SHELL_GATE === "always" || process.env.MANUAL_MODE_SHELL_GATE === "classify"
738
- ? { manualModeShellGate: process.env.MANUAL_MODE_SHELL_GATE }
739
- : {}),
740
- toolDeferLongtail: boolEnv("TOOL_DEFER_LONGTAIL", false),
741
- lspEnabled: boolEnv("LSP_ENABLED", false),
742
- lspHostEnabled,
743
- drainGraceMs: Math.max(10_000, numEnv("DRAIN_GRACE_MS", String(10 * 60 * 1000))),
744
- sighupIdleGraceMs: Math.max(5_000, numEnv("SIGHUP_IDLE_GRACE_MS", String(120_000))),
745
- mcpElicitation: {
746
- enabled: boolEnv("MCP_ELICITATION_ENABLED", false),
747
- throttle: {
748
- maxConcurrentPerRun: numEnvBounded("MCP_ELICITATION_MAX_CONCURRENT", String(DEFAULT_ELICITATION_THROTTLE.maxConcurrentPerRun), 1, 64),
749
- maxTotalPerRun: numEnvBounded("MCP_ELICITATION_MAX_TOTAL", String(DEFAULT_ELICITATION_THROTTLE.maxTotalPerRun), 1, 10_000),
750
- minIntervalMsPerServer: numEnvBounded("MCP_ELICITATION_MIN_INTERVAL_MS", String(DEFAULT_ELICITATION_THROTTLE.minIntervalMsPerServer), 0, 600_000),
751
- ttlMs: numEnvBounded("MCP_ELICITATION_TTL_MS", String(DEFAULT_ELICITATION_THROTTLE.ttlMs), 1_000, 3_600_000),
752
- },
753
- },
754
- askQuestionEnabled: postureOn("ASK_QUESTION_ENABLED"),
755
- toolApprovalEnabled: postureOn("TOOL_APPROVAL_ENABLED"),
756
- workflowAgentsReadOnly: boolEnv("WORKFLOW_AGENTS_READONLY", false),
1087
+ sessionAutoTitle: boolEnv("SESSION_AUTO_TITLE", true), // session auto-title: default ON (one cheap call per session)
1088
+ selectEnvironmentTool: boolEnv("SELECT_ENVIRONMENT_TOOL", true), // RFC A2: default ON (mount additionally gated on k8s+catalog)
1089
+ envFactsEnabled: boolEnv("SANDBOX_ENV_FACTS", true), // RFC A1: default ON (core 1.240.0 TaskSpec.envFacts; no-facts = no field)
1090
+ toolDeferLongtail: boolEnv("TOOL_DEFER_LONGTAIL", false), // [803]④ defer face: EXPERIMENTAL, default OFF
1091
+ lspEnabled: boolEnv("LSP_ENABLED", false), // sandbox lane: opt-in (needs a baked sema-code-lsp template)
1092
+ lspHostEnabled, // host lane: LSP_HOST_ENABLED, DEFAULT ON (CC-parity, degrades gracefully) — see the derivation above
1093
+ workflowAgentsReadOnly: boolEnv("WORKFLOW_AGENTS_READONLY", false), // [824]① TOB 保守旋钮:默认 off = workflow 子 agent 同权(CC parity)
757
1094
  imageBakes: {
758
1095
  enabled: boolEnv("IMAGE_BAKES_ENABLED", false),
759
1096
  runnerPrincipal: bakeRunnerPrincipal,
@@ -765,49 +1102,80 @@ export function loadConfig() {
765
1102
  submitRateMax: Math.max(0, numEnv("BAKE_SUBMIT_RATE_MAX", "10")),
766
1103
  submitRateWindowSec: Math.max(1, numEnv("BAKE_SUBMIT_RATE_WINDOW_SEC", "60")),
767
1104
  },
768
- toolTrace: boolEnv("TOOL_TRACE", false),
769
- traceThinking: boolEnv("TRACE_THINKING", true),
770
- logLevel: env("LOG_LEVEL", "info"),
1105
+ };
1106
+ }
1107
+ /** 域:limits+http(限额与 HTTP 面)—— 端口、附件/工作区体积与 TTL、成本与速率天花板、排空窗、回收周期。 */
1108
+ function parseLimitsHttpDomain() {
1109
+ return {
1110
+ port: Number(env("PORT", "8090")),
1111
+ // D-1 孤儿对象 GC grace(复审 F10;0=关掉本腿,负/坏值走默认)。
1112
+ attachmentOrphanGraceMs: ((v) => (v !== undefined && Number.isFinite(v) && v >= 0 ? v : 3_600_000))(process.env.ATTACHMENT_ORPHAN_GRACE_MS !== undefined ? Number(process.env.ATTACHMENT_ORPHAN_GRACE_MS) : undefined),
1113
+ // workspace 浏览面(#3,[1894]①)单文件读上限。
1114
+ workspaceFileMaxBytes: optFinitePositiveEnv("WORKSPACE_FILE_MAX_BYTES") ?? 8 * 1024 * 1024,
1115
+ attachmentMaxBytes: Math.max(1024, numEnv("ATTACHMENT_MAX_BYTES", String(32 * 1024 * 1024))),
1116
+ ...(process.env.ATTACHMENT_MIME_ALLOWLIST
1117
+ ? { attachmentMimeAllowlist: process.env.ATTACHMENT_MIME_ALLOWLIST.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean) }
1118
+ : {}),
1119
+ attachmentUnboundTtlMs: Math.max(60_000, numEnv("ATTACHMENT_UNBOUND_TTL_MS", String(24 * 3600 * 1000))),
1120
+ infraCostRates: {
1121
+ toolCallMicroUsd: Math.max(0, numEnv("INFRA_COST_TOOL_CALL", "0")),
1122
+ sandboxSecMicroUsd: Math.max(0, numEnv("INFRA_COST_SANDBOX_SEC", "0")),
1123
+ egressGbMicroUsd: Math.max(0, numEnv("INFRA_COST_EGRESS_GB", "0")),
1124
+ },
1125
+ drainGraceMs: Math.max(10_000, numEnv("DRAIN_GRACE_MS", String(10 * 60 * 1000))), // SIGTERM drain window, default 10min, floor 10s
1126
+ sighupIdleGraceMs: Math.max(5_000, numEnv("SIGHUP_IDLE_GRACE_MS", String(120_000))), // B3: SIGHUP idle-shutdown window, default 2min, floor 5s
1127
+ // BL-9: numEnv (not bare Number) — a non-numeric typo FAILS at startup instead of becoming NaN, which would
1128
+ // silently DISABLE the operator cost/rate ceiling (every `> ceiling` comparison against NaN is false).
771
1129
  rateLimitPerMin: numEnv("RATE_LIMIT_RPM", "0"),
772
1130
  maxTaskCostUsd: numEnv("MAX_TASK_COST_USD", "0"),
773
1131
  maxTaskTokens: numEnv("MAX_TASK_TOKENS", "0"),
774
1132
  maxPrincipalCostUsd: numEnv("MAX_PRINCIPAL_COST_USD", "0"),
775
1133
  costQuotaWindowSec: numEnv("COST_QUOTA_WINDOW_SEC", "86400"),
776
- degrade: process.env.MODEL_DEGRADE_TO
777
- ? {
778
- to: env("MODEL_DEGRADE_TO"),
779
- atCostFraction: Number(env("MODEL_DEGRADE_AT_COST_FRACTION", "0.7")),
780
- reactive: boolEnv("MODEL_DEGRADE_REACTIVE", false),
781
- downgradeOn: csv("MODEL_DEGRADE_ON").length
782
- ? csv("MODEL_DEGRADE_ON")
783
- : undefined,
784
- toSupportsImages: boolEnv("MODEL_DEGRADE_TO_VISION", false),
785
- }
786
- : undefined,
1134
+ reapIntervalSec: numEnv("REAP_INTERVAL_SEC", "60"),
1135
+ runStaleSec: numEnv("REAP_RUN_STALE_SEC", "120"),
1136
+ toolResultTtlSec: Number(env("TOOL_RESULT_TTL_SEC", "86400")),
1137
+ };
1138
+ }
1139
+ /** 域:observability(可观测)—— 指标/追踪令牌、工具与思考轨迹开关、日志档位、OTLP 出口。 */
1140
+ function parseObservabilityDomain() {
1141
+ return {
1142
+ metricsToken: process.env.METRICS_TOKEN || undefined,
1143
+ traceToken: process.env.TRACE_TOKEN || undefined,
1144
+ toolTrace: boolEnv("TOOL_TRACE", false),
1145
+ traceThinking: boolEnv("TRACE_THINKING", true), // default ON (clay)
1146
+ logLevel: env("LOG_LEVEL", "info"),
787
1147
  otel: process.env.OTEL_EXPORTER_OTLP_ENDPOINT
788
1148
  ? {
789
1149
  endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT.replace(/\/$/, ""),
790
1150
  intervalMs: Number(env("OTEL_EXPORT_INTERVAL_MS", "15000")),
791
1151
  serviceName: env("OTEL_SERVICE_NAME", "sema-server"),
1152
+ // `OTEL_EXPORTER_OTLP_HEADERS` = comma-separated `k=v` pairs (e.g. an auth header).
792
1153
  headers: Object.fromEntries(csv("OTEL_EXPORTER_OTLP_HEADERS").map((kv) => {
793
1154
  const i = kv.indexOf("=");
794
1155
  return i === -1 ? [kv, ""] : [kv.slice(0, i).trim(), kv.slice(i + 1).trim()];
795
1156
  })),
796
1157
  }
797
1158
  : undefined,
798
- approvalTimeoutSec: numEnv("APPROVAL_TIMEOUT_SEC", "0"),
799
- reapIntervalSec: numEnv("REAP_INTERVAL_SEC", "60"),
800
- runStaleSec: numEnv("REAP_RUN_STALE_SEC", "120"),
801
- syncImportLeaseStaleSec: Math.max(0, numEnv("SYNC_IMPORT_LEASE_STALE_SEC", "600")),
802
- toolResultTtlSec: Number(env("TOOL_RESULT_TTL_SEC", "86400")),
1159
+ };
1160
+ }
1161
+ /** 域:misc-integration(外部集成)—— plugins clone 白名单、git/OA 面、场景与 skill 目录、sema-registry 接入。 */
1162
+ function parseIntegrationsDomain() {
1163
+ return {
1164
+ // plugins 域 clone host 白名单(契约 [1361]①;缺省 github.com,逗号扩 https 镜像域)。
1165
+ pluginsAllowHosts: (process.env.PLUGINS_ALLOW_HOSTS ?? "github.com").split(",").map((h) => h.trim()).filter(Boolean),
1166
+ configBootFetchBudgetMs: Math.max(0, numEnv("CONFIG_BOOT_FETCH_BUDGET_MS", "1500")), // boot 首拉抢答窗;过窗转后台补齐(clay 2026-07-17:本地 5s 启动=黑洞中心同步等待)
803
1167
  gitApiBaseUrl: process.env.GIT_API_BASEURL,
804
1168
  gitApiToken: process.env.GIT_API_TOKEN,
1169
+ // BEHAVIOR CHANGE([891] clay 硬裁定):出厂缺省场景 default→code(CC 编码 persona 蒸馏版)——
1170
+ // 编码产品出厂态对标 CC 出厂态,通用域中性 persona 用 DEFAULT_SCENARIO=default 显式选回。
805
1171
  defaultScenario: env("DEFAULT_SCENARIO", "code"),
806
1172
  skillsDir: env("SKILLS_DIR", defaultSkillsDir()),
807
1173
  oaApiBaseUrl: process.env.OA_API_BASEURL,
808
1174
  oaServiceToken: process.env.OA_SERVICE_TOKEN,
1175
+ // 开 issue 工具:默认开到 OA 系统仓(AI-Only/oa-asset-system),写权限 token 走独立 env。
809
1176
  oaIssue: process.env.OA_ISSUE_TOKEN
810
1177
  ? {
1178
+ // npm 卫生纪律:不再烤内网 git 主机缺省 — 开了 OA_ISSUE_TOKEN 就必须给 host(fail-loud)。
811
1179
  baseUrl: (() => {
812
1180
  const u = process.env.OA_ISSUE_BASEURL ?? process.env.GIT_API_BASEURL;
813
1181
  if (!u)
@@ -820,6 +1188,11 @@ export function loadConfig() {
820
1188
  defaultLabels: csv("OA_ISSUE_LABELS").map(Number).filter((n) => Number.isInteger(n)),
821
1189
  }
822
1190
  : undefined,
1191
+ // sema-registry (control plane) URL/token/worker. The orchestrator renamed the worker-injected env from
1192
+ // CONFIG_CENTER_* → SEMA_REGISTRY_* (the control-plane host was renamed to match); it dual-injects
1193
+ // both names at the SAME value during the transition, so we DUAL-READ here — new name preferred, old name
1194
+ // fallback — and keep working after the orchestrator drops the legacy CONFIG_CENTER_* names. Pure config
1195
+ // plumbing: no API/auth/task-logic change.
823
1196
  configCenter: (process.env.SEMA_REGISTRY_URL || process.env.CONFIG_CENTER_URL)
824
1197
  ? {
825
1198
  baseUrl: process.env.SEMA_REGISTRY_URL || env("CONFIG_CENTER_URL"),
@@ -832,6 +1205,206 @@ export function loadConfig() {
832
1205
  configLocalDir: process.env.CONFIG_LOCAL_DIR || undefined,
833
1206
  };
834
1207
  }
1208
+ const STORE_GROUP_KEYS = [
1209
+ "sessionBackend", "sessionCacheTtlSec", "rewindSnapshotMaxMb", "dbBackend", "dbBackendExplicit", "localDataRoot",
1210
+ "tidb", "pg", "dbQueryTimeoutMs", "snapshotBlobStore", "snapshotBlobSqlMaxBytes", "snapshotBlobAllowSql", "sendUserFile",
1211
+ ];
1212
+ const MODEL_PLANE_GROUP_KEYS = [
1213
+ "gatewayBaseUrl", "gatewayApiKey", "gatewayFallbackUrls", "anthropic", "resilience", "model", "models",
1214
+ "modelApiKeyEnv", "modelApiKeys", "modelQuotaWeights", "tiers", "projects", "roles", "cascadeLadder", "degrade",
1215
+ ];
1216
+ const APPROVAL_GROUP_KEYS = [
1217
+ "approvalRequire", "approvalDeny", "approvalPollMs", "approvalTimeoutSec", "approvalAutoBudget", "approvalNeverAuto",
1218
+ "approvalHmacKeys", "durableApproval", "directApprovalDoor", "directDoorActive", "resourceSuspend",
1219
+ "resourceSuspendTtlSec", "askQuestionEnabled", "toolApprovalEnabled", "mcpElicitation", "sensitiveWritePatterns",
1220
+ "manualModeShellGate",
1221
+ ];
1222
+ const MEMORY_GROUP_KEYS = [
1223
+ "memoryEngineEnabled", "memoryEngineDir", "memoryEngineRemoteLaneAllowed", "memoryEngineBackend", "memoryScope",
1224
+ "memorySync", "projectMemoryEnabled", "syncImportLeaseStaleSec",
1225
+ ];
1226
+ const AUTH_GROUP_KEYS = [
1227
+ "authToken", "authTokens", "allowUnauthedWrites", "corsOrigins", "principalHeader", "requirePrincipal", "autonomy",
1228
+ "commandPolicy", "operatorPrincipals", "principalJwtPubkeys", "principalJwtIss", "principalJwtAud",
1229
+ "principalJwtMaxTtlSec", "bindHost",
1230
+ ];
1231
+ const ORCHESTRATION_GROUP_KEYS = [
1232
+ "remoteExec", "worktreeIsolation", "leaderEnabled", "leaderFanoutEnabled", "routerEnabled", "selfOrchestrationEnabled",
1233
+ "selfOrchestrationModels", "selfOrchestrationWorkerIsolation", "forkEnabled", "experimentalObserverAgents",
1234
+ "schedulerEnabled", "schedulerSessionWakeup", "schedulerStorePath", "planModeEnabled", "workflowRunStoreBackend",
1235
+ "workflowOrphanGraceMs", "workflowJournalRetentionMs", "workflowRunRetentionMs", "workflowAgentsReadOnly",
1236
+ "workflowSizeGuideline", "backgroundAgentRetentionMs", "backgroundAgentStaleRunningMs",
1237
+ "backgroundAgentParkClaimStaleMs", "rosterRetentionMs", "scratchpadSweepTtlMs", "sandboxPkgSource",
1238
+ "sessionAutoTitle", "selectEnvironmentTool", "envFactsEnabled", "toolDeferLongtail", "lspEnabled", "lspHostEnabled",
1239
+ "imageBakes",
1240
+ ];
1241
+ const LIMITS_HTTP_GROUP_KEYS = [
1242
+ "port", "attachmentOrphanGraceMs", "workspaceFileMaxBytes", "attachmentMaxBytes", "attachmentMimeAllowlist",
1243
+ "attachmentUnboundTtlMs", "infraCostRates", "drainGraceMs", "sighupIdleGraceMs", "rateLimitPerMin", "maxTaskCostUsd",
1244
+ "maxTaskTokens", "maxPrincipalCostUsd", "costQuotaWindowSec", "reapIntervalSec", "runStaleSec", "toolResultTtlSec",
1245
+ ];
1246
+ const OBSERVABILITY_GROUP_KEYS = ["metricsToken", "traceToken", "toolTrace", "traceThinking", "logLevel", "otel"];
1247
+ const INTEGRATIONS_GROUP_KEYS = [
1248
+ "pluginsAllowHosts", "mcpServers", "configBootFetchBudgetMs", "gitApiBaseUrl", "gitApiToken", "defaultScenario",
1249
+ "skillsDir", "oaApiBaseUrl", "oaServiceToken", "oaIssue", "configCenter", "configProvider", "configLocalDir",
1250
+ ];
1251
+ /** 组名 → 该组取景的平铺键(introspection 面:测试用它钉「每个平铺键恰好被一组取景」)。 */
1252
+ export const CONFIG_GROUP_KEYS = {
1253
+ store: STORE_GROUP_KEYS,
1254
+ modelPlane: MODEL_PLANE_GROUP_KEYS,
1255
+ approval: APPROVAL_GROUP_KEYS,
1256
+ memory: MEMORY_GROUP_KEYS,
1257
+ auth: AUTH_GROUP_KEYS,
1258
+ orchestration: ORCHESTRATION_GROUP_KEYS,
1259
+ limitsHttp: LIMITS_HTTP_GROUP_KEYS,
1260
+ observability: OBSERVABILITY_GROUP_KEYS,
1261
+ integrations: INTEGRATIONS_GROUP_KEYS,
1262
+ };
1263
+ /** 一组的**取景器**:每个键一个 accessor,读的时候才去平铺面取值。
1264
+ * ⚠️ 刻意不是 `{...pick(flat, keys)}` 的快照——快照会在 sema-registry 热应用(`config.model = …`、
1265
+ * `mutateInPlace(config.models, …)`)之后变成一份**静默过期**的旧值;取景器没有第二份存储,故不存在
1266
+ * 过期这个状态。`create*` 而非 `build*`:返回的是带行为(accessor)的活对象,不是纯数据。 */
1267
+ /** ⚠️ 语义边界(复审 2026-07-30 F4):组视图给**每个注册键**都装访问器,不看平铺面是否真有值——
1268
+ * `"tidb" in config.store` 恒 true、`Object.keys(config.store).length` 恒为注册表长度。本仓的
1269
+ * presence-as-semantics 判定(`"k" in x` / Object.keys 计数)**只能打平铺面**,组视图仅供点号读值。 */
1270
+ function createGroupView(flat, keys) {
1271
+ const view = {};
1272
+ for (const key of keys) {
1273
+ Object.defineProperty(view, key, { get: () => flat[key], enumerable: true, configurable: true });
1274
+ }
1275
+ return view;
1276
+ }
1277
+ /** 把九个组视图挂到平铺配置上(**就地**,返回的就是同一个对象——视图必须看着这一个存储处)。
1278
+ *
1279
+ * 组键是**不可枚举**的:`Object.keys(config)` / `JSON.stringify(config)` / `{...config}` 的平铺面因此
1280
+ * 逐字节不变(既有消费点、既有 dump/审计面零影响),代价是 spread 克隆不带组视图——克隆后要用组形,
1281
+ * 再过一次本函数(读一个缺席的组是当场 TypeError,不会静默给旧值)。
1282
+ *
1283
+ * `loadConfig` 是主调用点;测试夹具(test/helpers/make-config.ts)构造 `ServiceConfigFlat` 后同样过它。 */
1284
+ export function attachConfigGroups(flat) {
1285
+ const groups = {
1286
+ store: createGroupView(flat, STORE_GROUP_KEYS),
1287
+ modelPlane: createGroupView(flat, MODEL_PLANE_GROUP_KEYS),
1288
+ approval: createGroupView(flat, APPROVAL_GROUP_KEYS),
1289
+ memory: createGroupView(flat, MEMORY_GROUP_KEYS),
1290
+ auth: createGroupView(flat, AUTH_GROUP_KEYS),
1291
+ orchestration: createGroupView(flat, ORCHESTRATION_GROUP_KEYS),
1292
+ limitsHttp: createGroupView(flat, LIMITS_HTTP_GROUP_KEYS),
1293
+ observability: createGroupView(flat, OBSERVABILITY_GROUP_KEYS),
1294
+ integrations: createGroupView(flat, INTEGRATIONS_GROUP_KEYS),
1295
+ };
1296
+ for (const [name, view] of Object.entries(groups)) {
1297
+ Object.defineProperty(flat, name, { value: view, enumerable: false, writable: false, configurable: true });
1298
+ }
1299
+ return flat;
1300
+ }
1301
+ export function loadConfig() {
1302
+ CONFIG_WARNINGS.length = 0; // repeated loadConfig() calls (test setup) must not accumulate stale warnings
1303
+ CONFIG_NOTICES.length = 0;
1304
+ CONFIG_KNOBS.clear(); // the polarity table describes THIS load, never a previous one's env
1305
+ KNOB_DEPRECATIONS_SEEN.clear();
1306
+ // Posture gate: single-user turnkey (`REQUIRE_PRINCIPAL !== "true"` = one trusted super-admin on their own
1307
+ // box/tenant, CC's trust model) ⇒ the CC capability set defaults ON, each composed with its infra prereq; multi-tenant
1308
+ // (requirePrincipal) stays opt-in (gated + secured). This is the SAME discriminator LSP's host-default-ON uses,
1309
+ // generalized into one posture switch (the PM's "not per-flag" ask). In `postureOn`, an explicit `X_ENABLED=true/false`
1310
+ // ALWAYS wins; else the default = single-user ∧ infra-ready. 🔒 Multi-tenant (prod) is UNAFFECTED (postureOn → false).
1311
+ // Parsed ONCE here (design/158 B4) instead of re-comparing `process.env.REQUIRE_PRINCIPAL` at the four sites
1312
+ // that needed it — two of which spelled it `!== "true"` and two `=== "true"`, i.e. the same knob read in both
1313
+ // polarities within one function. `boolEnv(x, false)` is value-identical to the old `=== "true"`.
1314
+ const requirePrincipal = boolEnv("REQUIRE_PRINCIPAL", false);
1315
+ const singleUserTurnkey = !requirePrincipal;
1316
+ // `postureOn` takes the NAME (not the pre-read value) so the knob can register itself in the polarity table —
1317
+ // the posture family is precisely the one whose default is NOT readable from its name, so leaving it out of the
1318
+ // self-report would omit the worst offenders. Its tri-state semantics are unchanged: an explicit literal wins,
1319
+ // anything else (including a typo — deliberately NOT warned here, unlike boolEnv) falls to the posture default.
1320
+ const postureOn = (name, infraReady = true) => {
1321
+ const envVal = process.env[name];
1322
+ const explicit = envVal === "true" ? true : envVal === "false" ? false : undefined;
1323
+ const value = explicit ?? (singleUserTurnkey && infraReady);
1324
+ registerKnob({ name, polarity: "posture", value, source: explicit === undefined ? "posture" : "env" });
1325
+ return value;
1326
+ };
1327
+ // ── 九域解析(纯解析)。调用次序贴合改前的 env 读序,故 CONFIG_NOTICES 的产出顺序逐条不变。
1328
+ const store = parseStoreDomain({ requirePrincipal });
1329
+ const modelPlane = parseModelDomain();
1330
+ const approval = parseApprovalDomain({ postureOn });
1331
+ const memory = parseMemoryDomain({ requirePrincipal });
1332
+ const auth = parseAuthDomain({ requirePrincipal });
1333
+ const orchestration = parseOrchestrationDomain({ postureOn });
1334
+ const limitsHttp = parseLimitsHttpDomain();
1335
+ const observability = parseObservabilityDomain();
1336
+ const integrations = parseIntegrationsDomain();
1337
+ // ── 跨域耦合与跨域不变量:两条门(D-G 直连门、bake 门)的判据各自横跨 approval/auth/orchestration 三域,
1338
+ // 任何单域都不足以裁定,故留在装配层(旧名保留,断言与消息逐字不变)。
1339
+ const { directApprovalDoor, durableApproval: dgDurable, approvalHmacKeys: dgHmacKeys } = approval;
1340
+ const { principalJwtPubkeys, principalJwtIss, principalJwtAud, operatorPrincipals: dgOperators, authTokens } = auth;
1341
+ const { enabled: imageBakesEnabled, runnerPrincipal: bakeRunnerPrincipal } = orchestration.imageBakes;
1342
+ // 🔴 the direct-door crypto gate lives ONLY in the durable (checkpointStore) /decide branch — so DURABLE_APPROVAL
1343
+ // is a HARD precondition. Without it, checkpointStore is undefined, that branch is skipped, and the LEGACY
1344
+ // approvalStore /decide (trusted-header, NO crypto) becomes the live path = a full bypass (adversarial CRITICAL).
1345
+ const directDoorActive = directApprovalDoor && dgDurable && dgHmacKeys.length > 0 && principalJwtPubkeys.length > 0 && !!principalJwtIss && !!principalJwtAud;
1346
+ // 🔴 boot invariant (D-G §3): a half-configured direct door is the WORST state (looks on, verifies nothing) — and
1347
+ // an empty OPERATOR_PRINCIPALS makes isOperator() true-for-all (= cross-tenant escalation). Refuse to start.
1348
+ if (directApprovalDoor) {
1349
+ const missing = [];
1350
+ if (!dgDurable)
1351
+ missing.push("DURABLE_APPROVAL=true (the direct door's crypto gate only exists on the durable /decide path)");
1352
+ if (dgHmacKeys.length === 0)
1353
+ missing.push("APPROVAL_HMAC_KEYS");
1354
+ if (principalJwtPubkeys.length === 0)
1355
+ missing.push("PRINCIPAL_JWT_PUBKEYS");
1356
+ if (!principalJwtIss)
1357
+ missing.push("PRINCIPAL_JWT_ISS");
1358
+ if (!principalJwtAud)
1359
+ missing.push("PRINCIPAL_JWT_AUD");
1360
+ if (dgOperators.length === 0)
1361
+ missing.push("OPERATOR_PRINCIPALS (else isOperator is true-for-all)");
1362
+ // 🔴 F-fix (dim-6): a direct door is inherently MULTI-TENANT (per-task verified JWT principal). Without
1363
+ // REQUIRE_PRINCIPAL=true an absent/invalid principal falls through as ANONYMOUS and the owner/quota checks
1364
+ // degrade to the spoofable header for null-owner rows — refuse a direct door that doesn't require a principal.
1365
+ if (!requirePrincipal)
1366
+ missing.push("REQUIRE_PRINCIPAL=true (a direct door is multi-tenant; without it an absent principal is anonymous and the spoofable header governs owner/quota checks)");
1367
+ if (missing.length) {
1368
+ throw new Error(`DIRECT_APPROVAL_DOOR=true but D-G anchors are missing: ${missing.join(", ")} — refusing to start a half-open direct door (design/80 D-G boot invariant)`);
1369
+ }
1370
+ }
1371
+ // 🔴 boot invariant (IMAGE-API-DESIGN.md §P2.4a): the bake door is build-host-RCE-capable, and isOperator([],p)
1372
+ // is true-for-all — so enabling bakes with an empty OPERATOR_PRINCIPALS = a WORLD-WRITABLE RCE door. Refuse to
1373
+ // start (never a half-open bake door). The explicit-operator gate in server.ts is the runtime twin of this.
1374
+ if (imageBakesEnabled && dgOperators.length === 0) {
1375
+ throw new Error("IMAGE_BAKES_ENABLED=true but OPERATOR_PRINCIPALS is empty — the bake door runs build.sh on the privileged build host; an empty operator set makes isOperator() true-for-all (world-writable RCE). Set OPERATOR_PRINCIPALS or disable bakes (IMAGE-API-DESIGN.md §P2.4a boot invariant)");
1376
+ }
1377
+ // 🔴 boot invariant (IMAGE-API-DESIGN.md §P2.12 / H1): the bake-runner identity is DERIVED from its bearer
1378
+ // credential (token-derived `source`), never a self-asserted header — so a SERVICE_AUTH_TOKENS entry whose
1379
+ // system == BAKE_RUNNER_PRINCIPAL MUST exist, else the runner can never satisfy isRunner and the entire
1380
+ // claim/ingest/heartbeat pipeline is silently dead. Refuse to start a bake door with no runner credential.
1381
+ if (imageBakesEnabled && !Object.values(authTokens).includes(bakeRunnerPrincipal)) {
1382
+ throw new Error(`IMAGE_BAKES_ENABLED=true but no SERVICE_AUTH_TOKENS entry maps to the runner principal '${bakeRunnerPrincipal}' — the bake-runner authenticates by its credential (token-derived source), so a '<BAKE_RUNNER_TOKEN>=${bakeRunnerPrincipal}' SERVICE_AUTH_TOKENS entry is required or claim/ingest/heartbeat all 403 (IMAGE-API-DESIGN.md §P2.12 boot invariant)`);
1383
+ }
1384
+ // 平铺面**单点展开**九域产物(唯一存储处),再挂上九个组视图。组视图是对这同一个对象的取景器,
1385
+ // 不是第二份拷贝 —— 故 `config.limitsHttp.port === config.port` 恒成立,含热应用之后(见
1386
+ // attachConfigGroups / config-types.ts 分组段)。
1387
+ return attachConfigGroups({
1388
+ ...store,
1389
+ ...modelPlane,
1390
+ ...approval,
1391
+ ...memory,
1392
+ ...auth,
1393
+ ...orchestration,
1394
+ ...limitsHttp,
1395
+ ...observability,
1396
+ ...integrations,
1397
+ directDoorActive, // 跨域合取(approval × auth × requirePrincipal),见上
1398
+ });
1399
+ }
1400
+ /** 有效监听绑址([1934])。**显式 `BIND_HOST`/`HOST` 恒生效**(operator 保留在任何形下明示暴露的
1401
+ * 权利);未显式时:**无鉴权写面 ⇒ 127.0.0.1**,其余 ⇒ undefined(Node 默认全接口,既有部署零影响)。
1402
+ *
1403
+ * 为什么缺省要自己收窄、而不是只加旋钮:桌面形的安全结论原本写着「safe ONLY because HOST is
1404
+ * pinned to 127.0.0.1」,而那个前提在本仓从未成立(HOST 零消费)——把一个没人实现的假设当防线,
1405
+ * 是「沉默陷阱」。零凭证 + 全接口 + host lane(用户真机非沙箱)= 同网段任意执行,缺省必须安全。
1406
+ * "无鉴权写面" 的判据与 http 层 503 fail-closed 门同源:allowUnauthedWrites 且**完全没有**凭证
1407
+ * (半配=凭证必须使用,不算无鉴权)。 */
835
1408
  export function resolveBindHost(config) {
836
1409
  if (config.bindHost)
837
1410
  return config.bindHost;