@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
@@ -1,36 +1,109 @@
1
+ /**
2
+ * SVC-3 — git-WORKTREE ISOLATION wrapper for the deployment-level `executionEnvFactory` (design/97
3
+ * CORE-6). A higher-order factory that mints a per-AGENT isolated git worktree when (and ONLY
4
+ * when) core threads `ctx.isolation === "worktree"` through the TRUSTED `RunInternals` channel.
5
+ *
6
+ * WHY this lives in service, not core: core ships the env-AGNOSTIC primitive (`addWorktree` / `pruneWorktrees`
7
+ * on the base `ExecutionEnv` seam, plus the `ExecutionEnvFactoryContext.isolation` hint). The env-SPECIFIC
8
+ * bits — which concrete adapter roots an env at the worktree dir, which repo is the operator-trusted base,
9
+ * where the reaper runs — are deployment wiring, so they belong here (铁律 core-service-boundary). We consume
10
+ * the core export; we do NOT reimplement the git plumbing.
11
+ *
12
+ * SHAPE (per the task contract):
13
+ * const factory = withWorktreeIsolation(baseFactory, opts);
14
+ * - ctx.isolation === "worktree": `addWorktree(baseEnvForGit, { repoRoot, sessionId: ctx.sessionId,
15
+ * rootEnvAt, commit? })` → a worktree-rooted env WITH `destroy()` (the Runner calls it on task end →
16
+ * `git worktree remove --force`, which tears down ONLY that worktree, never the base repo).
17
+ * - otherwise: `baseFactory(ctx)` verbatim — the SHARED base env, NO `destroy` (the Runner must never tear
18
+ * the base down). Non-isolated tasks behave exactly as before this wrapper existed.
19
+ *
20
+ * 🔴 TRUST GATE (the whole point of SVC-3): the ONLY input that turns isolation ON is core's
21
+ * `ctx.isolation` — populated from `ctx.agent({ isolation })` via the TRUSTED `RunInternals`, which core
22
+ * guarantees is NEVER sourced from the untrusted `TaskSpec` (see core remote-env.ts §isolation). This wrapper
23
+ * reads NOTHING else from `ctx` to decide isolation, and `repoRoot` comes ENTIRELY from the operator-supplied
24
+ * closure (config), never from any task/`ctx` field — so an untrusted caller can neither flip isolation on a
25
+ * repo it shouldn't touch nor inject a `repoRoot` path to escape the allowed roots. We additionally assert the
26
+ * configured `repoRoot` is contained within an operator-trusted `allowedRoots` set at wrap time (defence in
27
+ * depth: a mis-wired deployment fails loud rather than git-worktree-ing an arbitrary path).
28
+ *
29
+ * 🔴 ISOLATE-ONLY (clay): the runtime NEVER merges a worktree back. The orchestrator script reads each
30
+ * worktree's result (via the worktree dir / the snapshot) and decides verify+merge in USERLAND. We only mint
31
+ * and reap; merge policy is not ours.
32
+ *
33
+ * ⚠️ BOUNDARY CAVEATS (from core's CORE-6 doc — kept here so a future wirer doesn't reintroduce
34
+ * a footgun):
35
+ * - durable-suspend is INCOMPATIBLE with a worktree env: core's prepare-task rejects a NON-remote factory env
36
+ * for a suspendable run (a host/NodeExecutionEnv-rooted worktree is not snapshotable). So an isolated agent
37
+ * MUST run to completion without parking — do NOT combine `isolation:"worktree"` with a suspend/HITL park
38
+ * on the host lane. (On an isolated lane like e2b/k8s the worktree would have to root a REMOTE env; that is
39
+ * a follow-on, see `rootEnvAt` below — host lane is v1.)
40
+ * - `git worktree` isolates the git WORKING COPY, not arbitrary shell access: bash inside the worktree can
41
+ * still reach the host filesystem/network. The worktree is a coordination boundary, NOT a security sandbox
42
+ * — pair it with a real sandbox lane (e2b/k8s/local-docker) when isolation from the host is required.
43
+ * - UNCOMMITTED changes in the base repo are ABSENT in the child worktree (core creates it `--detach` at
44
+ * `HEAD`/`commit`). An isolated agent sees the committed tree only — commit (or stash→apply, in userland)
45
+ * before fanning out if the base's dirty state must be visible.
46
+ * - a worktree orphaned by a process CRASH (the Runner never reached `destroy`) is reaped by
47
+ * {@link pruneWorktrees} — wired into the service reaper (best-effort, userland; core ships no post-kill
48
+ * Runner hook).
49
+ */
1
50
  import path from "node:path";
2
51
  import { addWorktree, pruneWorktrees, } from "@sema-agent/core";
52
+ /** Normalize a path for containment checks: resolve to absolute + drop a trailing separator. */
3
53
  function norm(p) {
4
54
  const r = path.resolve(p);
5
55
  return r.length > 1 && r.endsWith(path.sep) ? r.slice(0, -1) : r;
6
56
  }
57
+ /** True when `child` is `root` itself or a descendant of it (segment-aware: `/a/b` is NOT under `/a/bc`). */
7
58
  function isWithin(root, child) {
8
59
  const r = norm(root);
9
60
  const c = norm(child);
10
61
  if (c === r)
11
62
  return true;
12
63
  const rel = path.relative(r, c);
64
+ // path.relative returns "" for equal, a non-"../"-leading relative for a descendant, and a "../"-leading
65
+ // (or absolute, on Windows drive change) path for an outside/sibling target.
13
66
  return rel.length > 0 && !rel.startsWith("..") && !path.isAbsolute(rel);
14
67
  }
68
+ /**
69
+ * Wrap a deployment's `executionEnvFactory` so an agent with `ctx.isolation === "worktree"` runs in its own
70
+ * detached git worktree. See the file header for the full contract + trust gate + caveats.
71
+ *
72
+ * Throws at WRAP time (fail-loud, not faked) if `repoRoot` is not contained within `allowedRoots` — a
73
+ * mis-wired deployment must not silently git-worktree an arbitrary path.
74
+ */
15
75
  export function withWorktreeIsolation(baseFactory, opts) {
16
76
  const repoRoot = norm(opts.repoRoot);
17
77
  const allowedRoots = (opts.allowedRoots?.length ? opts.allowedRoots : [repoRoot]).map(norm);
78
+ // Trust gate (defence in depth): the configured repoRoot must live inside an operator-trusted root. This is
79
+ // a CONFIG invariant (repoRoot is never a task input), so a violation = mis-wire → fail loud at boot.
18
80
  if (!allowedRoots.some((root) => isWithin(root, repoRoot))) {
19
81
  throw new Error(`withWorktreeIsolation: repoRoot ${repoRoot} is not within any allowedRoots [${allowedRoots.join(", ")}] — refusing to git-worktree an untrusted path`);
20
82
  }
83
+ // SERIALIZE git worktree ADMIN ops on this shared repo. `git worktree add`/`remove` mutate the repo's
84
+ // `.git/worktrees` registry under a repo-level lock; running them CONCURRENTLY against one repo races (a
85
+ // parallel fan-out finishing together → the Runner calls several `destroy()` at once → some
86
+ // `git worktree remove` lose the lock and leave the worktree dir+registration behind — an orphan the
87
+ // dir-only `pruneWorktrees` can't reap). A per-wrapper (= per-repoRoot) promise-chain mutex chains every
88
+ // add/remove so they apply one at a time (git ops are fast + serial-safe; the agents themselves still run
89
+ // fully in parallel — only the ~ms git bookkeeping serializes).
21
90
  let gitLock = Promise.resolve();
22
91
  const serialize = (fn) => {
23
- const run = gitLock.then(fn, fn);
92
+ const run = gitLock.then(fn, fn); // run after the prior op SETTLES (success OR failure — never deadlock)
24
93
  gitLock = run.then(() => undefined, () => undefined);
25
94
  return run;
26
95
  };
27
96
  return async (ctx) => {
97
+ // TRUST GATE: the ONLY trigger is core's RunInternals-sourced ctx.isolation. We read nothing else from ctx
98
+ // (and never anything from a TaskSpec) to decide isolation — see file header.
28
99
  if (ctx.isolation !== "worktree") {
100
+ // Non-isolated: the SHARED base env, verbatim, NO destroy attached → the Runner never tears the base
101
+ // down (core's hasDestroy() returns false for a plain base env). Behaviour identical to no wrapper.
29
102
  return baseFactory(ctx);
30
103
  }
31
104
  const { env, worktreeDir } = await serialize(() => addWorktree(opts.baseEnvForGit, {
32
105
  repoRoot,
33
- sessionId: ctx.sessionId,
106
+ sessionId: ctx.sessionId, // core sanitizes this into the worktree dir name (human-readable orphan diag)
34
107
  rootEnvAt: opts.rootEnvAt,
35
108
  ...(opts.commit ? { commit: opts.commit } : {}),
36
109
  }));
@@ -39,28 +112,54 @@ export function withWorktreeIsolation(baseFactory, opts) {
39
112
  ...(ctx.taskId ? { taskId: ctx.taskId } : {}),
40
113
  worktreeDir,
41
114
  });
115
+ // Wrap core's `destroy()` (runs `git worktree remove`) through the SAME mutex so a concurrent teardown can't
116
+ // race the registry lock and orphan the worktree. The other env methods (exec/fs) keep their own binding —
117
+ // we only override `destroy`. Idempotent at the git level (remove of an already-gone worktree is a no-op).
118
+ //
119
+ // POST-MORTEM + RETRY (2026-07-26, the deterministic pin for the 30-round reaper-race intermittent):
120
+ // core's destroy is FAIL-OPEN on the remove — `git-worktree-env.js` neither checks the exec Result nor lets a
121
+ // rejection out (`.catch(() => undefined)`), so one transient failure (spawn EAGAIN/EMFILE under full-suite
122
+ // load) resolves destroy() normally while leaving dir+registration behind — and `git worktree prune` can NEVER
123
+ // reap a worktree whose dir still exists, so the orphan is permanent. Until core checks the result (reported),
124
+ // we verify the dir is actually gone after each destroy, retry ONCE on residue, and FAIL LOUD if it survives
125
+ // both — a thrown teardown is diagnosable; a silently-accumulating orphan tree is not.
42
126
  const coreDestroy = env.destroy.bind(env);
43
127
  const residue = async () => {
44
128
  const r = await opts.baseEnvForGit.exists(worktreeDir);
45
- return r.ok ? r.value : false;
129
+ return r.ok ? r.value : false; // can't verify (base env unhealthy) → don't invent a residue; destroy stays best-effort
46
130
  };
131
+ // IDEMPOTENCY (core 1.423): the remove leg is now FAIL-LOUD ([1796]§二 → [1801] — the fail-open this wrapper's
132
+ // post-mortem was built against is fixed), so a REPEAT destroy would hit `git worktree remove` on an
133
+ // already-removed worktree and reject. A completed destroy + no residue ⇒ short-circuit (the goal state is
134
+ // already reached; double/concurrent destroy is a legal Runner shape). The first call always goes through
135
+ // coreDestroy (the inner env's own teardown must run).
47
136
  let destroyedOnce = false;
48
137
  env.destroy = () => serialize(async () => {
49
138
  if (destroyedOnce && !(await residue()))
50
- return;
139
+ return; // idempotent repeat — done is done
51
140
  await coreDestroy();
52
141
  destroyedOnce = true;
53
142
  if (!(await residue()))
54
143
  return;
55
144
  opts.logger?.warn?.("worktree_destroy_residue_retry", { worktreeDir, sessionId: ctx.sessionId });
56
- await coreDestroy();
145
+ await coreDestroy(); // second shot on a live registration/dir — safe; a missing one is short-circuited above
57
146
  if (await residue()) {
58
147
  throw new Error(`worktree destroy left residue at ${worktreeDir} (git worktree remove failed twice — see core git-worktree-env fail-open)`);
59
148
  }
60
149
  });
61
- return env;
150
+ return env; // carries the serialized destroy() → core's Runner removes the worktree (ONLY this one) on task end.
62
151
  };
63
152
  }
153
+ /**
154
+ * Best-effort reaper hook: deregister worktrees orphaned by a process crash (the Runner never reached
155
+ * `destroy`). Core's {@link pruneWorktrees} runs `git worktree prune` against the base repo — it cleans up
156
+ * registrations whose worktree dirs are already gone. Wired into the service reaper (userland; core ships no
157
+ * post-kill Runner hook). NEVER throws (a prune failure is logged, never fatal).
158
+ *
159
+ * NOTE: `git worktree prune` only deregisters worktrees whose DIRECTORY is missing. A crashed run that left
160
+ * the dir intact is NOT reaped by prune alone — that is acceptable for v1 (the worktree lives under the known
161
+ * `.sema-worktrees/` parent and is human-/script-reapable; a stricter age-based sweep is a follow-on).
162
+ */
64
163
  export async function reapOrphanWorktrees(baseEnvForGit, repoRoot, logger) {
65
164
  try {
66
165
  await pruneWorktrees(baseEnvForGit, norm(repoRoot));
@@ -1,10 +1,45 @@
1
+ /**
2
+ * Cross-replica write-behind aggregating counter — the shared mechanism behind the cost quota (P0-1)
3
+ * and the rate limiter (P0-2), on BOTH dialects. Both are additive per-key counters over an aligned fixed
4
+ * window, so the shared store uses an ATOMIC INCREMENT (`val = val + delta`), never last-writer-wins.
5
+ *
6
+ * design/158 A12 C1b: this file used to be MySQL-hardwired and the PG lane carried TWO inline copies of the
7
+ * same 150-line class (pg-cost-quota.ts's private `PgWriteBehindCounter` + pg-rate-limiter.ts's exported one).
8
+ * They are now ONE dialect-parameterized implementation; `WriteBehindCounter` (mysql2) and
9
+ * `PgWriteBehindCounter` (node-pg) remain as ctor subclasses so every call site and test keeps its shape.
10
+ *
11
+ * Hot path stays sync: `addLocal` accumulates a local pending delta, `used` reads (cached fleet total
12
+ * from the last refresh) + (this replica's unflushed delta). A periodic `flush` pushes deltas atomically
13
+ * and refreshes the current window's fleet totals into the cache.
14
+ *
15
+ * Consistency is **eventual** → this is **SOFT limiting**: a replica may briefly exceed the ceiling by
16
+ * up to (replicas × per-flush-interval spend/requests) at the boundary before peers' totals propagate.
17
+ * Right for cost/fairness ceilings (and backstopped, for cost, by the hard per-task `maxCostUsd` gate).
18
+ * A HARD compliance ceiling would instead need a linearizable CAS/atomic count, not write-behind — same
19
+ * trade as the cross-replica circuit breaker. Windows are aligned fixed buckets (`floor(now/windowMs)`),
20
+ * the standard distributed choice (a rolling per-key window is the hard part to coordinate cross-replica).
21
+ *
22
+ * ── Dialect deltas, kept EXPLICIT (the four the PG twins' file headers used to list) ──────────────────
23
+ * - placeholders `?` vs `$1..$n`
24
+ * - atomic increment: `ON DUPLICATE KEY UPDATE val = val + VALUES(val)` vs
25
+ * `ON CONFLICT (<keyCol>, window_bucket) DO UPDATE SET val = <table>.val + EXCLUDED.val`
26
+ * - affected rows: mysql2 `ResultSetHeader.affectedRows` vs node-pg `result.rowCount`
27
+ * - BIGINT/INT read back: node-pg returns them as STRINGS → always `Number(...)` on read (the read path
28
+ * already wraps every counter cell, so this is a no-op difference at the JS boundary)
29
+ * - per-query timeout ARG SHAPE: mysql2 takes `{sql, timeout}` as the first query arg; node-pg takes a
30
+ * `QueryConfig` with `query_timeout`. That is why this file keeps its own exec instead of using
31
+ * sql-driver.ts (which is on the plain `(sql, params)` form).
32
+ */
1
33
  import type { Pool as MySqlPool } from "mysql2/promise";
2
34
  import type { Pool as PgPool } from "pg";
3
35
  export interface CounterTable {
36
+ /** Table name; must have columns (<keyCol>, window_bucket BIGINT, <valCol>, updated_at). */
4
37
  table: string;
5
38
  keyCol: string;
6
39
  valCol: string;
7
40
  }
41
+ /** S9 (SILENT-FALLBACK P0-e): a flush/refresh failure notification. `streak` = consecutive failures of that
42
+ * kind (0 = recovered); `prevStreak` lets the consumer detect the recovery edge. Shared by both dialects. */
8
43
  export interface CounterDegradeInfo {
9
44
  table: string;
10
45
  kind: "flush" | "refresh" | "stalled";
@@ -13,8 +48,14 @@ export interface CounterDegradeInfo {
13
48
  error?: string;
14
49
  }
15
50
  export type CounterDegradeHook = (info: CounterDegradeInfo) => void;
51
+ /** S9 deeper fix: default per-query timeout for the counter family's statements. Always ON (unlike the
52
+ * opt-in pool-wide DB_QUERY_TIMEOUT_MS): every statement here is a tiny single-row/single-bucket op, so
53
+ * 30s is ~1000× headroom — a query that slow IS the S9 pathology (a DB-side hang would otherwise pin the
54
+ * flush round forever; the stalled watchdog can then only report, not recover). The rejection lands in
55
+ * the existing flush/refresh catch → the S9 streak telemetry counts it; the round ENDS and retries. */
16
56
  export declare const DEFAULT_COUNTER_QUERY_TIMEOUT_MS = 30000;
17
57
  type CounterDialect = "tidb" | "pg";
58
+ /** Dual-dialect write-behind aggregating counter. See the file header for the dialect-delta ledger. */
18
59
  export declare class SqlWriteBehindCounter {
19
60
  private readonly dialect;
20
61
  private readonly pool;
@@ -29,24 +70,38 @@ export declare class SqlWriteBehindCounter {
29
70
  private flushing;
30
71
  private flushStartedAt;
31
72
  private failStreak;
32
- constructor(dialect: CounterDialect, pool: MySqlPool | PgPool, t: CounterTable, windowMs: number, now?: () => number, onDegraded?: CounterDegradeHook | undefined, queryTimeoutMs?: number);
73
+ constructor(dialect: CounterDialect, pool: MySqlPool | PgPool, t: CounterTable, windowMs: number, now?: () => number, onDegraded?: CounterDegradeHook | undefined, // S9: observability tap — never affects counting
74
+ queryTimeoutMs?: number);
75
+ /** The ONLY place the two drivers' query shapes diverge. Returns {rows, affected}. */
33
76
  private exec;
77
+ /** S9: track the consecutive-failure streak per kind and notify on every failure + on the recovery edge
78
+ * (streak>0 → 0). Steady healthy state stays silent. The hook must never break the counter. */
34
79
  private bump;
35
80
  bucket(): number;
36
81
  private rk;
82
+ /** Cached fleet total (peers, from the last refresh) + this replica's unflushed delta, current window. */
37
83
  used(key: string): number;
38
84
  addLocal(key: string, n: number): void;
85
+ /** Seconds until the current aligned window rolls over. */
39
86
  retryAfterSec(): number;
87
+ /** Atomically push pending deltas, then refresh the current window's fleet totals + reap old buckets.
88
+ * Reentrancy-guarded: two overlapping flushes would each apply `+delta` from the same snapshot and
89
+ * double-count into the shared table (inflating fleet totals → spurious rate-limit/quota rejection). */
40
90
  flush(): Promise<void>;
41
91
  private flushOnce;
92
+ /** Load the current window's fleet totals into the cache (pendingDelta is separate → never clobbered). */
42
93
  refresh(): Promise<void>;
94
+ /** Delete expired window buckets to bound the table. */
43
95
  reap(): Promise<number>;
44
96
  startRefresh(intervalMs?: number): this;
45
97
  stop(): void;
46
98
  }
99
+ /** MySQL-protocol (TiDB) binding — historical class name + ctor shape preserved. */
47
100
  export declare class WriteBehindCounter extends SqlWriteBehindCounter {
48
101
  constructor(pool: MySqlPool, t: CounterTable, windowMs: number, now?: () => number, onDegraded?: CounterDegradeHook, queryTimeoutMs?: number);
49
102
  }
103
+ /** PostgreSQL binding — the class pg-rate-limiter.ts used to define inline (and pg-cost-quota.ts duplicated
104
+ * privately, hardwired to the cost_quota columns). Same ctor shape as the mysql2 twin. */
50
105
  export declare class PgWriteBehindCounter extends SqlWriteBehindCounter {
51
106
  constructor(pool: PgPool, t: CounterTable, windowMs: number, now?: () => number, onDegraded?: CounterDegradeHook, queryTimeoutMs?: number);
52
107
  }
@@ -1,5 +1,11 @@
1
- const SEP = "\u0000";
1
+ const SEP = "\u0000"; // NUL — can't appear in a principal / scope / IP key
2
+ /** S9 deeper fix: default per-query timeout for the counter family's statements. Always ON (unlike the
3
+ * opt-in pool-wide DB_QUERY_TIMEOUT_MS): every statement here is a tiny single-row/single-bucket op, so
4
+ * 30s is ~1000× headroom — a query that slow IS the S9 pathology (a DB-side hang would otherwise pin the
5
+ * flush round forever; the stalled watchdog can then only report, not recover). The rejection lands in
6
+ * the existing flush/refresh catch → the S9 streak telemetry counts it; the round ENDS and retries. */
2
7
  export const DEFAULT_COUNTER_QUERY_TIMEOUT_MS = 30_000;
8
+ /** Dual-dialect write-behind aggregating counter. See the file header for the dialect-delta ledger. */
3
9
  export class SqlWriteBehindCounter {
4
10
  dialect;
5
11
  pool;
@@ -8,13 +14,18 @@ export class SqlWriteBehindCounter {
8
14
  now;
9
15
  onDegraded;
10
16
  queryTimeoutMs;
11
- pendingDelta = new Map();
12
- fleetTotal = new Map();
17
+ pendingDelta = new Map(); // unflushed local, by rowKey (key + window bucket)
18
+ fleetTotal = new Map(); // peers' current-window view, refreshed periodically
13
19
  timer;
14
20
  flushing = false;
15
- flushStartedAt = 0;
16
- failStreak = new Map();
17
- constructor(dialect, pool, t, windowMs, now = () => Date.now(), onDegraded, queryTimeoutMs = DEFAULT_COUNTER_QUERY_TIMEOUT_MS) {
21
+ flushStartedAt = 0; // S9 review MED-1: hang-watchdog anchor // reentrancy guard: a flush slower than the timer interval must not overlap
22
+ failStreak = new Map(); // S9: consecutive-failure runs, per kind
23
+ constructor(dialect, pool, t, windowMs, now = () => Date.now(), onDegraded, // S9: observability tap — never affects counting
24
+ // S9 deeper fix: per-query inactivity timeout. mysql2's is the client-side `{sql, timeout}` form (the
25
+ // connection is held until the hung command really ends, so a poisoned connection is never reused
26
+ // mid-query); node-pg's is `query_timeout` on the QueryConfig. Overridable via DB_QUERY_TIMEOUT_MS
27
+ // (threaded by store-backend.ts); the default keeps the counter face always bounded.
28
+ queryTimeoutMs = DEFAULT_COUNTER_QUERY_TIMEOUT_MS) {
18
29
  this.dialect = dialect;
19
30
  this.pool = pool;
20
31
  this.t = t;
@@ -23,6 +34,7 @@ export class SqlWriteBehindCounter {
23
34
  this.onDegraded = onDegraded;
24
35
  this.queryTimeoutMs = queryTimeoutMs;
25
36
  }
37
+ /** The ONLY place the two drivers' query shapes diverge. Returns {rows, affected}. */
26
38
  async exec(sql, params) {
27
39
  if (this.dialect === "tidb") {
28
40
  const [r] = (await this.pool.query({ sql, timeout: this.queryTimeoutMs }, params));
@@ -34,6 +46,8 @@ export class SqlWriteBehindCounter {
34
46
  const res = await this.pool.query(q);
35
47
  return { rows: (res.rows ?? []), affected: res.rowCount ?? 0 };
36
48
  }
49
+ /** S9: track the consecutive-failure streak per kind and notify on every failure + on the recovery edge
50
+ * (streak>0 → 0). Steady healthy state stays silent. The hook must never break the counter. */
37
51
  bump(kind, failed, error) {
38
52
  if (!this.onDegraded)
39
53
  return;
@@ -46,6 +60,7 @@ export class SqlWriteBehindCounter {
46
60
  this.onDegraded({ table: this.t.table, kind, streak: next, prevStreak: prev, ...(failed && error !== undefined ? { error: error instanceof Error ? error.message : String(error) } : {}) });
47
61
  }
48
62
  catch {
63
+ /* observability must never break the counter */
49
64
  }
50
65
  }
51
66
  bucket() {
@@ -54,6 +69,7 @@ export class SqlWriteBehindCounter {
54
69
  rk(key) {
55
70
  return `${key}${SEP}${this.bucket()}`;
56
71
  }
72
+ /** Cached fleet total (peers, from the last refresh) + this replica's unflushed delta, current window. */
57
73
  used(key) {
58
74
  const rk = this.rk(key);
59
75
  return (this.fleetTotal.get(rk) ?? 0) + (this.pendingDelta.get(rk) ?? 0);
@@ -64,11 +80,20 @@ export class SqlWriteBehindCounter {
64
80
  const rk = this.rk(key);
65
81
  this.pendingDelta.set(rk, (this.pendingDelta.get(rk) ?? 0) + n);
66
82
  }
83
+ /** Seconds until the current aligned window rolls over. */
67
84
  retryAfterSec() {
68
85
  return Math.ceil(((this.bucket() + 1) * this.windowMs - this.now()) / 1000);
69
86
  }
87
+ /** Atomically push pending deltas, then refresh the current window's fleet totals + reap old buckets.
88
+ * Reentrancy-guarded: two overlapping flushes would each apply `+delta` from the same snapshot and
89
+ * double-count into the shared table (inflating fleet totals → spurious rate-limit/quota rejection). */
70
90
  async flush() {
71
91
  if (this.flushing) {
92
+ // S9 review MED-1: a hung query holds this guard: every later tick returns HERE and bump() is never
93
+ // reached, so the reject-path telemetry goes silent exactly when the fleet is degraded. Surface the
94
+ // stall itself — >60s inside one flush round = a "stalled" failure signal per tick; its streak clears
95
+ // when the round finally settles. (The per-query timeout above is the deeper fix — a round now self-
96
+ // terminates at queryTimeoutMs; this watchdog stays as the backstop for anything it doesn't cover.)
72
97
  if (this.now() - this.flushStartedAt > 60_000)
73
98
  this.bump("stalled", true, "flush round stalled >60s (hung query?)");
74
99
  return;
@@ -80,17 +105,18 @@ export class SqlWriteBehindCounter {
80
105
  }
81
106
  finally {
82
107
  this.flushing = false;
83
- this.bump("stalled", false);
108
+ this.bump("stalled", false); // watchdog recovery edge (no-op if it never fired)
84
109
  }
85
110
  }
86
111
  async flushOnce() {
87
112
  const { table, keyCol, valCol } = this.t;
113
+ // The atomic-increment upsert — the ONE statement whose dialect delta actually matters here.
88
114
  const sql = this.dialect === "tidb"
89
115
  ? `INSERT INTO ${table} (${keyCol}, window_bucket, ${valCol}, updated_at) VALUES (?,?,?,?) ` +
90
116
  `ON DUPLICATE KEY UPDATE ${valCol} = ${valCol} + VALUES(${valCol}), updated_at = VALUES(updated_at)`
91
117
  : `INSERT INTO ${table} (${keyCol}, window_bucket, ${valCol}, updated_at) VALUES ($1,$2,$3,$4) ` +
92
118
  `ON CONFLICT (${keyCol}, window_bucket) DO UPDATE SET ${valCol} = ${table}.${valCol} + EXCLUDED.${valCol}, updated_at = EXCLUDED.updated_at`;
93
- let attempted = false;
119
+ let attempted = false; // S9: only score a flush round that actually tried a write
94
120
  let failure;
95
121
  let hadFailure = false;
96
122
  for (const [rk, delta] of [...this.pendingDelta]) {
@@ -101,7 +127,8 @@ export class SqlWriteBehindCounter {
101
127
  const windowBucket = Number(rk.slice(sep + 1));
102
128
  attempted = true;
103
129
  try {
104
- await this.exec(sql, [key, windowBucket, delta, new Date()]);
130
+ await this.exec(sql, [key, windowBucket, delta, new Date()]); // S9 deeper fix: a hung INSERT rejects into the catch below (streak counts it)
131
+ // subtract what we flushed (not delete — a concurrent addLocal may have grown it meanwhile)
105
132
  const remaining = (this.pendingDelta.get(rk) ?? 0) - delta;
106
133
  if (remaining > 0)
107
134
  this.pendingDelta.set(rk, remaining);
@@ -109,15 +136,22 @@ export class SqlWriteBehindCounter {
109
136
  this.pendingDelta.delete(rk);
110
137
  }
111
138
  catch (err) {
112
- hadFailure = true;
139
+ hadFailure = true; // S9: surfaced via bump() below — the retry/soft-limit semantics are unchanged
113
140
  failure = err;
141
+ // keep the delta; retry next flush (advisory — never throw into the caller).
142
+ // KNOWN/ACCEPTED (audit B): an ambiguous failure (INSERT applied but the ack was lost) re-adds
143
+ // the delta next flush = double-count; conversely a reader between our INSERT and a peer's sees
144
+ // a momentary under-count. Both are bounded by one flush window and this counter backs SOFT
145
+ // limits only (quota/rate-limit, design: eventual consistency; hard caps use per-task budgets).
146
+ // Exactly-once would need a flush ledger/CAS — not worth it at this tier.
114
147
  }
115
148
  }
116
149
  if (attempted)
117
- this.bump("flush", hadFailure, failure);
150
+ this.bump("flush", hadFailure, failure); // S9: empty rounds carry no health signal
118
151
  await this.refresh();
119
152
  await this.reap();
120
153
  }
154
+ /** Load the current window's fleet totals into the cache (pendingDelta is separate → never clobbered). */
121
155
  async refresh() {
122
156
  const { table, keyCol, valCol } = this.t;
123
157
  const b = this.bucket();
@@ -127,15 +161,17 @@ export class SqlWriteBehindCounter {
127
161
  : `SELECT ${keyCol}, ${valCol} FROM ${table} WHERE window_bucket = $1`, [b]);
128
162
  const next = new Map();
129
163
  for (const row of rows) {
130
- next.set(`${String(row[keyCol])}${SEP}${b}`, Number(row[valCol]));
164
+ next.set(`${String(row[keyCol])}${SEP}${b}`, Number(row[valCol])); // node-pg returns BIGINT/INT as STRING
131
165
  }
132
166
  this.fleetTotal = next;
133
- this.bump("refresh", false);
167
+ this.bump("refresh", false); // S9: recovery edge (no-op while healthy)
134
168
  }
135
169
  catch (err) {
136
- this.bump("refresh", true, err);
170
+ // keep last-known cache on a transient read failure
171
+ this.bump("refresh", true, err); // S9
137
172
  }
138
173
  }
174
+ /** Delete expired window buckets to bound the table. */
139
175
  async reap() {
140
176
  try {
141
177
  const { affected } = await this.exec(this.dialect === "tidb"
@@ -158,11 +194,14 @@ export class SqlWriteBehindCounter {
158
194
  this.timer = undefined;
159
195
  }
160
196
  }
197
+ /** MySQL-protocol (TiDB) binding — historical class name + ctor shape preserved. */
161
198
  export class WriteBehindCounter extends SqlWriteBehindCounter {
162
199
  constructor(pool, t, windowMs, now, onDegraded, queryTimeoutMs) {
163
200
  super("tidb", pool, t, windowMs, now, onDegraded, queryTimeoutMs);
164
201
  }
165
202
  }
203
+ /** PostgreSQL binding — the class pg-rate-limiter.ts used to define inline (and pg-cost-quota.ts duplicated
204
+ * privately, hardwired to the cost_quota columns). Same ctor shape as the mysql2 twin. */
166
205
  export class PgWriteBehindCounter extends SqlWriteBehindCounter {
167
206
  constructor(pool, t, windowMs, now, onDegraded, queryTimeoutMs) {
168
207
  super("pg", pool, t, windowMs, now, onDegraded, queryTimeoutMs);
@@ -1,4 +1,10 @@
1
1
  import type { ApprovalHmacKey, PrincipalJwtKey } from "./auth-keys.js";
2
+ /**
3
+ * design/80 D-G: verify a compact JWS principal token. Returns the crypto-verified principal (`sub`) + the
4
+ * channel-binding (`bnd`) on success, or `null` on ANY failure (caller fail-closes — NEVER falls back to a header).
5
+ * 🔴 Hardening: the JWS header `alg` MUST equal the kid's PINNED alg (rejects `alg:none`, symmetric, downgrade);
6
+ * the kid MUST resolve to a configured key; iss/aud/exp/nbf (with a small skew) are enforced; `sub` MUST be present.
7
+ */
2
8
  export declare function verifyPrincipalJwt(token: string, keys: PrincipalJwtKey[], expect: {
3
9
  iss: string;
4
10
  aud: string;
@@ -9,11 +15,29 @@ export declare function verifyPrincipalJwt(token: string, keys: PrincipalJwtKey[
9
15
  sub: string;
10
16
  bnd?: string;
11
17
  } | null;
18
+ /** The channel-binding value a direct client's principal JWT must carry in `cnf.bnd`: the hex SHA-256 of the
19
+ * CLIENT-VISIBLE action identity (sessionId + boundCallId + boundInputHash — NOT the unexposed checkpointToken).
20
+ * Binds the identity assertion to THIS specific suspended action so a captured JWT cannot be lifted onto another. */
12
21
  export declare function approvalBnd(env: {
13
22
  sessionId: string;
14
23
  boundCallId?: string | null;
15
24
  boundInputHash?: string | null;
16
25
  }): string;
26
+ /**
27
+ * design/80 D-G: verify the WHOLE direct-door proof for one /decide — the signed principal JWT (identity) + its
28
+ * cnf.bnd channel-binding + the HMAC envelope (integrity) — and return the CRYPTO-VERIFIED principal, or a typed
29
+ * reject. 🔴 Fail-closed: every failure rejects (the caller must NOT fall back to the trusted header). Order:
30
+ * verify principal → bind → mac-present → mac-valid (so a non-operator never gets a MAC oracle, an unverified
31
+ * caller never reaches the operator list).
32
+ *
33
+ * 🔴 `opts.actionBinding`: the boundCallId/boundInputHash action-binding is `policy_ask`-
34
+ * SPECIFIC (a pending TOOL call). A NON-ACTION gate — `plan_review` (and any review-pause) — `binds NO action`
35
+ * (core checkpoint-store.ts: pendingAction `{kind:"plan_review"}`, editedPlan is a typed sibling that binds no
36
+ * action), so the proof correctly DEGRADES to "verify WHO": JWT identity + cnf.bnd channel-binding + the HMAC over
37
+ * {sessionId, decision} (bound fields null) — WITHOUT requiring a boundInputHash. Pass `actionBinding:false` for a
38
+ * non-action gate; the default (true) keeps the /decide tool-approval contract byte-identical. Binding the decision
39
+ * to a plan HASH was REJECTED (the plan artifact lives in a profile ref store; the worker has no TOCTOU original).
40
+ */
17
41
  export declare function verifyDirectDoorProof(proof: {
18
42
  jwt?: string;
19
43
  mac?: string;
@@ -1,8 +1,29 @@
1
+ /**
2
+ * design/80 D-G: principal crypto-binding (the direct-door IDENTITY gate) + the combined direct-door proof
3
+ * verifier. Split out of `security.ts` (design/158 A13, internal-lossless — the facade re-exports every symbol
4
+ * below unchanged; `verifyDirectDoorProof` imports {@link verifyApprovalHmac} from `./approval-hmac.js` for the
5
+ * integrity half of the combined proof; `security.ts`'s own `verifiedPrincipal` imports {@link verifyPrincipalJwt}
6
+ * from here for the direct-door identity branch).
7
+ *
8
+ * ✅ WIRED (was "🔴 SCAFFOLD — NOT YET WIRED" — STALE, corrected by the 2026-07-25 doc-rot sweep). Two live call
9
+ * sites, both traced: `verifiedPrincipal` (security.ts) takes the direct-door branch when `directDoorActive` and
10
+ * derives the principal from {@link verifyPrincipalJwt} instead of the header, and {@link verifyDirectDoorProof}
11
+ * verifies the same JWT for the decide routes. Still true, and the part worth keeping: `directDoorActive` is
12
+ * computed from the anchors (config.ts) and is OFF by default, so the direct branch never runs in a stock
13
+ * deployment — activation = provisioning the issuer JWKS + iss/aud, a deployment task, not a coding one.
14
+ * The direct-connect /decide door must NEVER trust a client-supplied principal header
15
+ * (invariant #1). Instead the client presents a SIGNED JWT (sub = principal), and the worker verifies the SIGNATURE
16
+ * against the issuer's PUBLIC key — so identity is crypto-DERIVED, not header-asserted. PUBLIC keys ⇒ non-secret
17
+ * (ride the config projection like OPERATOR_PRINCIPALS, not the HMAC secret path). The issuer's PRIVATE key lives
18
+ * ONLY with the trusted minter (the BFF/identity service), never on a worker. HMAC stays integrity; this is identity.
19
+ */
1
20
  import { createHash, createPublicKey, verify as cryptoVerify } from "node:crypto";
2
21
  import { verifyApprovalHmac } from "./approval-hmac.js";
3
22
  function b64urlToBuf(s) {
4
23
  return Buffer.from(s, "base64url");
5
24
  }
25
+ /** Map a PINNED JWS alg → the node:crypto verify call. EdDSA(Ed25519): algorithm=null. RS256: RSA-PKCS1+sha256.
26
+ * ES256: ECDSA P-256 with the JWS raw r||s encoding (dsaEncoding ieee-p1363, NOT DER). Returns false on mismatch. */
6
27
  function verifyJwsSignature(alg, signingInput, sig, pub) {
7
28
  try {
8
29
  if (alg === "EdDSA")
@@ -17,6 +38,12 @@ function verifyJwsSignature(alg, signingInput, sig, pub) {
17
38
  return false;
18
39
  }
19
40
  }
41
+ /**
42
+ * design/80 D-G: verify a compact JWS principal token. Returns the crypto-verified principal (`sub`) + the
43
+ * channel-binding (`bnd`) on success, or `null` on ANY failure (caller fail-closes — NEVER falls back to a header).
44
+ * 🔴 Hardening: the JWS header `alg` MUST equal the kid's PINNED alg (rejects `alg:none`, symmetric, downgrade);
45
+ * the kid MUST resolve to a configured key; iss/aud/exp/nbf (with a small skew) are enforced; `sub` MUST be present.
46
+ */
20
47
  export function verifyPrincipalJwt(token, keys, expect) {
21
48
  if (keys.length === 0)
22
49
  return null;
@@ -35,13 +62,15 @@ export function verifyPrincipalJwt(token, keys, expect) {
35
62
  catch {
36
63
  return null;
37
64
  }
65
+ // A payload of the JSON literal `null` (or any non-object) parses WITHOUT throwing — guard before any deref so a
66
+ // crafted `base64url("null").<payload>.<sig>` token fail-closes to a typed reject, not an uncaught TypeError → 500.
38
67
  if (typeof header !== "object" || header === null || typeof payload !== "object" || payload === null)
39
68
  return null;
40
69
  if (typeof header.alg !== "string" || typeof header.kid !== "string" || sig.length === 0)
41
70
  return null;
42
71
  const key = keys.find((k) => k.kid === header.kid);
43
72
  if (!key || key.alg !== header.alg)
44
- return null;
73
+ return null; // 🔴 alg PINNED to the kid's key — no confusion/downgrade
45
74
  let pub;
46
75
  try {
47
76
  pub = createPublicKey(key.key);
@@ -52,6 +81,7 @@ export function verifyPrincipalJwt(token, keys, expect) {
52
81
  const signingInput = Buffer.from(`${h}.${p}`, "utf8");
53
82
  if (!verifyJwsSignature(key.alg, signingInput, sig, pub))
54
83
  return null;
84
+ // claims — signature verified, now enforce the envelope
55
85
  if (payload.iss !== expect.iss)
56
86
  return null;
57
87
  const aud = payload.aud;
@@ -62,32 +92,57 @@ export function verifyPrincipalJwt(token, keys, expect) {
62
92
  return null;
63
93
  const now = expect.nowSec ?? Math.floor(Date.now() / 1000);
64
94
  const skew = expect.skewSec ?? 30;
95
+ // 🔴 exp is MANDATORY (the design mandates a short-lived token, ≤120s) — a missing/non-numeric exp must FAIL, else a
96
+ // token minted without exp would never expire (anti-replay = single-use checkpointToken ONLY, with no time bound).
65
97
  if (typeof payload.exp !== "number" || payload.exp + skew < now)
66
- return null;
98
+ return null; // missing / non-numeric / expired
67
99
  if (payload.nbf !== undefined && (typeof payload.nbf !== "number" || payload.nbf - skew > now))
68
- return null;
100
+ return null; // nbf, if present, must be numeric + reached
101
+ // Cap the TTL so a sloppy/over-permissive minter can't widen the replay window past the short-lived mandate.
69
102
  if (expect.maxTtlSec && payload.exp - now > expect.maxTtlSec + skew)
70
103
  return null;
71
104
  const bnd = typeof payload.cnf?.bnd === "string" ? payload.cnf.bnd : undefined;
72
105
  return { sub: payload.sub, bnd };
73
106
  }
107
+ /** The channel-binding value a direct client's principal JWT must carry in `cnf.bnd`: the hex SHA-256 of the
108
+ * CLIENT-VISIBLE action identity (sessionId + boundCallId + boundInputHash — NOT the unexposed checkpointToken).
109
+ * Binds the identity assertion to THIS specific suspended action so a captured JWT cannot be lifted onto another. */
74
110
  export function approvalBnd(env) {
75
111
  return createHash("sha256").update(JSON.stringify([env.sessionId, env.boundCallId ?? null, env.boundInputHash ?? null])).digest("hex");
76
112
  }
113
+ /**
114
+ * design/80 D-G: verify the WHOLE direct-door proof for one /decide — the signed principal JWT (identity) + its
115
+ * cnf.bnd channel-binding + the HMAC envelope (integrity) — and return the CRYPTO-VERIFIED principal, or a typed
116
+ * reject. 🔴 Fail-closed: every failure rejects (the caller must NOT fall back to the trusted header). Order:
117
+ * verify principal → bind → mac-present → mac-valid (so a non-operator never gets a MAC oracle, an unverified
118
+ * caller never reaches the operator list).
119
+ *
120
+ * 🔴 `opts.actionBinding`: the boundCallId/boundInputHash action-binding is `policy_ask`-
121
+ * SPECIFIC (a pending TOOL call). A NON-ACTION gate — `plan_review` (and any review-pause) — `binds NO action`
122
+ * (core checkpoint-store.ts: pendingAction `{kind:"plan_review"}`, editedPlan is a typed sibling that binds no
123
+ * action), so the proof correctly DEGRADES to "verify WHO": JWT identity + cnf.bnd channel-binding + the HMAC over
124
+ * {sessionId, decision} (bound fields null) — WITHOUT requiring a boundInputHash. Pass `actionBinding:false` for a
125
+ * non-action gate; the default (true) keeps the /decide tool-approval contract byte-identical. Binding the decision
126
+ * to a plan HASH was REJECTED (the plan artifact lives in a profile ref store; the worker has no TOCTOU original).
127
+ */
77
128
  export function verifyDirectDoorProof(proof, envelope, config, opts) {
78
- const actionBinding = opts?.actionBinding ?? true;
129
+ const actionBinding = opts?.actionBinding ?? true; // default = the policy_ask tool-approval action-binding (unchanged)
79
130
  if (!proof.jwt)
80
131
  return { ok: false, status: 401, errorCode: "principal_unverified", error: "direct door requires a signed principal token (X-Approval-Principal-Token)" };
81
132
  const verified = verifyPrincipalJwt(proof.jwt, config.principalJwtPubkeys, { iss: config.principalJwtIss ?? "\0", aud: config.principalJwtAud ?? "\0", maxTtlSec: config.principalJwtMaxTtlSec });
82
133
  if (!verified)
83
134
  return { ok: false, status: 401, errorCode: "principal_unverified", error: "principal token failed verification" };
135
+ // action-binding gates (policy_ask) MUST carry the bound action; a non-action gate (plan_review) skips this — the
136
+ // cnf.bnd + HMAC below still bind {sessionId, decision} (with null bound fields), so WHO + decision-integrity hold.
84
137
  if (actionBinding && !envelope.boundInputHash)
85
138
  return { ok: false, status: 400, errorCode: "approval_mac_required", error: "direct door requires boundInputHash (the bound action) in the body" };
86
139
  if (verified.bnd !== approvalBnd(envelope))
87
140
  return { ok: false, status: 401, errorCode: "principal_unbound", error: "principal token is not bound to this decision (cnf.bnd mismatch)" };
88
141
  if (!proof.mac)
89
142
  return { ok: false, status: 401, errorCode: "approval_mac_required", error: "direct door requires an approval MAC (X-Approval-Mac)" };
90
- const ok = verifyApprovalHmac({ sessionId: envelope.sessionId, boundCallId: envelope.boundCallId ?? null, boundInputHash: envelope.boundInputHash ?? null, decision: envelope.decision, reason: envelope.reason ?? null }, proof.mac, proof.kid, config.approvalHmacKeys);
143
+ const ok = verifyApprovalHmac(
144
+ // reason 与两个绑定位同样归一到 null(缺席=null),使"服务端算的"与"签名器签的"在缺席形上逐字节一致。
145
+ { sessionId: envelope.sessionId, boundCallId: envelope.boundCallId ?? null, boundInputHash: envelope.boundInputHash ?? null, decision: envelope.decision, reason: envelope.reason ?? null }, proof.mac, proof.kid, config.approvalHmacKeys);
91
146
  if (!ok)
92
147
  return { ok: false, status: 401, errorCode: "approval_mac_invalid", error: "approval envelope MAC invalid" };
93
148
  return { ok: true, principal: verified.sub };