@sema-agent/server 1.322.0 → 2.0.0

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