@sema-agent/server 1.323.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (565) hide show
  1. package/dist/approval-hmac.d.ts +40 -0
  2. package/dist/approval-hmac.js +67 -0
  3. package/dist/approval.d.ts +31 -0
  4. package/dist/approval.js +72 -4
  5. package/dist/audit.d.ts +26 -0
  6. package/dist/audit.js +40 -1
  7. package/dist/auth-bridge.d.ts +10 -0
  8. package/dist/auth-bridge.js +47 -4
  9. package/dist/auth-keys.d.ts +19 -0
  10. package/dist/auth-keys.js +13 -0
  11. package/dist/bake-runner/main.d.ts +5 -0
  12. package/dist/bake-runner/main.js +43 -4
  13. package/dist/bake-runner/protocol.d.ts +57 -0
  14. package/dist/bake-runner/protocol.js +58 -3
  15. package/dist/bake-runner/runner.d.ts +54 -0
  16. package/dist/bake-runner/runner.js +87 -5
  17. package/dist/bench/l8/artifact.d.ts +63 -0
  18. package/dist/bench/l8/artifact.js +61 -2
  19. package/dist/bench/l8/escape.d.ts +36 -0
  20. package/dist/bench/l8/escape.js +25 -0
  21. package/dist/bench/l8/index.d.ts +14 -0
  22. package/dist/bench/l8/index.js +14 -0
  23. package/dist/bench/l8/probes.d.ts +74 -0
  24. package/dist/bench/l8/probes.js +60 -0
  25. package/dist/bench/l8/run-probes.d.ts +43 -0
  26. package/dist/bench/l8/run-probes.js +45 -2
  27. package/dist/bench/s1/arms.d.ts +126 -0
  28. package/dist/bench/s1/arms.js +188 -13
  29. package/dist/bench/s1/live-deps.d.ts +66 -0
  30. package/dist/bench/s1/live-deps.js +249 -11
  31. package/dist/bench/s1/oracle.d.ts +49 -0
  32. package/dist/bench/s1/oracle.js +36 -1
  33. package/dist/bench/s1/repair-oracle-adapter.d.ts +33 -0
  34. package/dist/bench/s1/repair-oracle-adapter.js +30 -0
  35. package/dist/bench/s1/reviewer.d.ts +38 -0
  36. package/dist/bench/s1/reviewer.js +34 -0
  37. package/dist/bench/s1/row.d.ts +77 -0
  38. package/dist/bench/s1/row.js +62 -0
  39. package/dist/bench/s1/run-firm.d.ts +47 -0
  40. package/dist/bench/s1/run-firm.js +70 -4
  41. package/dist/bench/s1/runner-ctx.d.ts +71 -0
  42. package/dist/bench/s1/runner-ctx.js +36 -0
  43. package/dist/bench/s1/tasks.d.ts +108 -0
  44. package/dist/bench/s1/tasks.js +115 -0
  45. package/dist/boot/budget-tracing.d.ts +48 -0
  46. package/dist/boot/budget-tracing.js +86 -0
  47. package/dist/boot/config-center.d.ts +62 -0
  48. package/dist/boot/config-center.js +995 -0
  49. package/dist/boot/coordinators.d.ts +33 -0
  50. package/dist/boot/coordinators.js +97 -0
  51. package/dist/boot/execution-env.d.ts +26 -0
  52. package/dist/boot/execution-env.js +370 -0
  53. package/dist/boot/leader.d.ts +27 -0
  54. package/dist/boot/leader.js +81 -0
  55. package/dist/boot/reapers.d.ts +53 -0
  56. package/dist/boot/reapers.js +252 -0
  57. package/dist/boot/resolve-spec.d.ts +70 -0
  58. package/dist/boot/resolve-spec.js +1072 -0
  59. package/dist/boot/runner-deps.d.ts +101 -0
  60. package/dist/boot/runner-deps.js +343 -0
  61. package/dist/boot/runtime-caps.d.ts +21 -0
  62. package/dist/boot/runtime-caps.js +62 -0
  63. package/dist/boot/session-faces.d.ts +57 -0
  64. package/dist/boot/session-faces.js +157 -0
  65. package/dist/boot/shutdown.d.ts +50 -0
  66. package/dist/boot/shutdown.js +129 -0
  67. package/dist/boot/stores.d.ts +32 -0
  68. package/dist/boot/stores.js +361 -0
  69. package/dist/boot/workflow-orchestration.d.ts +46 -0
  70. package/dist/boot/workflow-orchestration.js +150 -0
  71. package/dist/boot-reclaim.d.ts +19 -0
  72. package/dist/boot-reclaim.js +20 -0
  73. package/dist/brain.d.ts +25 -0
  74. package/dist/brain.js +61 -1
  75. package/dist/budget.d.ts +66 -0
  76. package/dist/budget.js +128 -4
  77. package/dist/capabilities/builtin-tools.d.ts +3 -0
  78. package/dist/capabilities/builtin-tools.js +3 -0
  79. package/dist/capabilities/center-plugins.d.ts +17 -1
  80. package/dist/capabilities/center-plugins.js +45 -4
  81. package/dist/capabilities/center-prompts.d.ts +46 -0
  82. package/dist/capabilities/center-prompts.js +57 -3
  83. package/dist/capabilities/code-review-council.d.ts +28 -0
  84. package/dist/capabilities/code-review-council.js +40 -7
  85. package/dist/capabilities/collab-wire.d.ts +2 -0
  86. package/dist/capabilities/collab-workflows.d.ts +7 -0
  87. package/dist/capabilities/collab-workflows.js +44 -1
  88. package/dist/capabilities/oa-tools.d.ts +6 -0
  89. package/dist/capabilities/oa-tools.js +10 -0
  90. package/dist/capabilities/prompt.d.ts +14 -0
  91. package/dist/capabilities/prompt.js +14 -0
  92. package/dist/capabilities/prompts/code-review.d.ts +14 -0
  93. package/dist/capabilities/prompts/code-review.js +14 -0
  94. package/dist/capabilities/prompts/identity.d.ts +6 -0
  95. package/dist/capabilities/prompts/identity.js +6 -0
  96. package/dist/capabilities/prompts/team.d.ts +4 -0
  97. package/dist/capabilities/prompts/team.js +4 -0
  98. package/dist/capabilities/repo-tools.d.ts +11 -0
  99. package/dist/capabilities/repo-tools.js +8 -0
  100. package/dist/capabilities/sandbox-file-send.d.ts +95 -0
  101. package/dist/capabilities/sandbox-file-send.js +91 -3
  102. package/dist/capabilities/scenarios.d.ts +130 -0
  103. package/dist/capabilities/scenarios.js +180 -7
  104. package/dist/capabilities/select-environment-tool.d.ts +7 -0
  105. package/dist/capabilities/select-environment-tool.js +30 -0
  106. package/dist/capabilities/send-user-file-tool.d.ts +33 -0
  107. package/dist/capabilities/send-user-file-tool.js +65 -1
  108. package/dist/capabilities/skills.d.ts +29 -0
  109. package/dist/capabilities/skills.js +13 -2
  110. package/dist/capabilities/team.d.ts +22 -0
  111. package/dist/capabilities/team.js +22 -2
  112. package/dist/capabilities/tool-defer.d.ts +26 -0
  113. package/dist/capabilities/tool-defer.js +3 -0
  114. package/dist/config-center/apply-effective.d.ts +65 -0
  115. package/dist/config-center/apply-effective.js +204 -12
  116. package/dist/config-center/http-client.d.ts +34 -0
  117. package/dist/config-center/http-client.js +68 -0
  118. package/dist/config-center/restart-signal.d.ts +22 -0
  119. package/dist/config-center/restart-signal.js +39 -0
  120. package/dist/config-center/skills-mcp.d.ts +20 -1
  121. package/dist/config-center/skills-mcp.js +42 -7
  122. package/dist/config-center/types.d.ts +92 -1
  123. package/dist/config-lkg.d.ts +12 -0
  124. package/dist/config-lkg.js +41 -0
  125. package/dist/config-provider.d.ts +78 -0
  126. package/dist/config-provider.js +165 -2
  127. package/dist/config-types.d.ts +585 -1
  128. package/dist/config.d.ts +78 -2
  129. package/dist/config.js +874 -301
  130. package/dist/degenerate-instrument.d.ts +38 -0
  131. package/dist/degenerate-instrument.js +55 -3
  132. package/dist/elicitation.d.ts +39 -0
  133. package/dist/elicitation.js +87 -3
  134. package/dist/env-facts.d.ts +61 -0
  135. package/dist/env-facts.js +73 -7
  136. package/dist/fleet/fleet-bus.d.ts +185 -0
  137. package/dist/fleet/fleet-bus.js +333 -27
  138. package/dist/fleet/subagent-tail-bus.d.ts +11 -0
  139. package/dist/fleet/subagent-tail-bus.js +33 -2
  140. package/dist/fleet-client.d.ts +38 -0
  141. package/dist/fleet-client.js +44 -9
  142. package/dist/fleet-lease.d.ts +46 -0
  143. package/dist/fleet-lease.js +73 -5
  144. package/dist/hooks/branch-transcript.d.ts +8 -0
  145. package/dist/hooks/branch-transcript.js +33 -0
  146. package/dist/hooks/cc-stop-prompt.d.ts +42 -0
  147. package/dist/hooks/cc-stop-prompt.js +43 -1
  148. package/dist/hooks/hook-llm.d.ts +15 -0
  149. package/dist/hooks/hook-llm.js +47 -0
  150. package/dist/hooks/hook-runner.d.ts +89 -0
  151. package/dist/hooks/hook-runner.js +327 -18
  152. package/dist/http/idempotency.d.ts +31 -0
  153. package/dist/http/idempotency.js +34 -0
  154. package/dist/http/principal-gate.d.ts +17 -0
  155. package/dist/http/principal-gate.js +35 -3
  156. package/dist/http/route-ctx.d.ts +36 -0
  157. package/dist/http/routes/approvals-assistant.d.ts +25 -0
  158. package/dist/http/routes/approvals-assistant.js +253 -23
  159. package/dist/http/routes/attachments.js +22 -4
  160. package/dist/http/routes/capabilities.d.ts +7 -0
  161. package/dist/http/routes/capabilities.js +202 -1
  162. package/dist/http/routes/fleet.d.ts +6 -0
  163. package/dist/http/routes/fleet.js +94 -5
  164. package/dist/http/routes/images.d.ts +17 -0
  165. package/dist/http/routes/images.js +222 -14
  166. package/dist/http/routes/leader.d.ts +7 -0
  167. package/dist/http/routes/leader.js +10 -0
  168. package/dist/http/routes/memory-policy.d.ts +6 -0
  169. package/dist/http/routes/memory-policy.js +38 -6
  170. package/dist/http/routes/notify-wake.d.ts +10 -0
  171. package/dist/http/routes/notify-wake.js +19 -3
  172. package/dist/http/routes/observability.d.ts +6 -0
  173. package/dist/http/routes/observability.js +19 -2
  174. package/dist/http/routes/runs.d.ts +15 -0
  175. package/dist/http/routes/runs.js +464 -41
  176. package/dist/http/routes/session-sync.d.ts +15 -0
  177. package/dist/http/routes/session-sync.js +229 -19
  178. package/dist/http/routes/sessions-list.d.ts +6 -0
  179. package/dist/http/routes/sessions-list.js +35 -4
  180. package/dist/http/routes/sessions.d.ts +14 -0
  181. package/dist/http/routes/sessions.js +176 -31
  182. package/dist/http/routes/side-query.d.ts +8 -0
  183. package/dist/http/routes/side-query.js +26 -1
  184. package/dist/http/routes/tasks.d.ts +18 -0
  185. package/dist/http/routes/tasks.js +501 -38
  186. package/dist/http/routes/trace-usage.d.ts +10 -0
  187. package/dist/http/routes/trace-usage.js +66 -14
  188. package/dist/http/routes/workflows.d.ts +12 -0
  189. package/dist/http/routes/workflows.js +180 -19
  190. package/dist/http/run-meta.d.ts +11 -0
  191. package/dist/http/run-meta.js +6 -0
  192. package/dist/http/send.d.ts +19 -0
  193. package/dist/http/send.js +11 -0
  194. package/dist/http/server.d.ts +255 -0
  195. package/dist/http/server.js +921 -75
  196. package/dist/http/sse-log.d.ts +15 -0
  197. package/dist/http/sse-log.js +16 -0
  198. package/dist/http/tar.d.ts +9 -0
  199. package/dist/http/tar.js +21 -5
  200. package/dist/http/wire-gate.d.ts +9 -0
  201. package/dist/http/wire-gate.js +9 -0
  202. package/dist/http/wire-types.d.ts +187 -0
  203. package/dist/http/workspace-content.d.ts +8 -0
  204. package/dist/http/workspace-content.js +10 -0
  205. package/dist/images/bake-validate.d.ts +63 -0
  206. package/dist/images/bake-validate.js +71 -1
  207. package/dist/images/manifest.d.ts +19 -0
  208. package/dist/images/manifest.js +6 -1
  209. package/dist/index.d.ts +1 -5
  210. package/dist/index.js +10 -5
  211. package/dist/key-resolver.d.ts +23 -0
  212. package/dist/key-resolver.js +27 -2
  213. package/dist/leader/diffout.d.ts +23 -0
  214. package/dist/leader/diffout.js +8 -1
  215. package/dist/leader/diffup.d.ts +32 -0
  216. package/dist/leader/diffup.js +43 -0
  217. package/dist/leader/endpoint.d.ts +20 -0
  218. package/dist/leader/endpoint.js +25 -2
  219. package/dist/leader/fanout.d.ts +60 -0
  220. package/dist/leader/fanout.js +29 -3
  221. package/dist/leader/grader-env-factory.d.ts +74 -0
  222. package/dist/leader/grader-env-factory.js +62 -3
  223. package/dist/leader/leader.d.ts +97 -0
  224. package/dist/leader/leader.js +151 -4
  225. package/dist/leader/merge.d.ts +94 -0
  226. package/dist/leader/merge.js +112 -9
  227. package/dist/leader/planner.d.ts +52 -0
  228. package/dist/leader/planner.js +47 -3
  229. package/dist/leader/repair-oracle.d.ts +61 -0
  230. package/dist/leader/repair-oracle.js +59 -1
  231. package/dist/leader/repair-wire.d.ts +78 -0
  232. package/dist/leader/repair-wire.js +79 -2
  233. package/dist/leader/wire.d.ts +79 -0
  234. package/dist/leader/wire.js +255 -8
  235. package/dist/lsp/e2b-bridge.d.ts +16 -0
  236. package/dist/lsp/e2b-bridge.js +32 -4
  237. package/dist/lsp/e2b-manager.d.ts +10 -0
  238. package/dist/lsp/e2b-manager.js +18 -2
  239. package/dist/lsp/lsp-frames.d.ts +12 -0
  240. package/dist/lsp/lsp-frames.js +12 -0
  241. package/dist/lsp/manager.d.ts +12 -0
  242. package/dist/lsp/manager.js +35 -2
  243. package/dist/lsp/types.d.ts +10 -0
  244. package/dist/lsp/ws-transport.d.ts +12 -0
  245. package/dist/lsp/ws-transport.js +53 -5
  246. package/dist/lsp-evict.d.ts +14 -0
  247. package/dist/lsp-evict.js +15 -1
  248. package/dist/main.js +530 -2129
  249. package/dist/memory-export.d.ts +1 -0
  250. package/dist/memory-export.js +4 -0
  251. package/dist/memory-scope.d.ts +49 -0
  252. package/dist/memory-scope.js +87 -4
  253. package/dist/memory-sync-client.d.ts +24 -0
  254. package/dist/memory-sync-client.js +41 -2
  255. package/dist/memory-sync.d.ts +23 -0
  256. package/dist/memory-sync.js +70 -1
  257. package/dist/model-select.d.ts +39 -0
  258. package/dist/model-select.js +42 -1
  259. package/dist/observability/cost-quota.d.ts +20 -0
  260. package/dist/observability/cost-quota.js +4 -0
  261. package/dist/observability/cost-taxonomy.d.ts +45 -0
  262. package/dist/observability/cost-taxonomy.js +34 -0
  263. package/dist/observability/logger.d.ts +1 -0
  264. package/dist/observability/logger.js +6 -0
  265. package/dist/observability/metrics.d.ts +13 -0
  266. package/dist/observability/metrics.js +63 -0
  267. package/dist/observability/otel-exporter.d.ts +9 -0
  268. package/dist/observability/otel-exporter.js +11 -3
  269. package/dist/observability/principal-context.d.ts +9 -0
  270. package/dist/observability/principal-context.js +9 -0
  271. package/dist/observability/prompt-manifest.d.ts +37 -0
  272. package/dist/observability/prompt-manifest.js +35 -1
  273. package/dist/observability/rate-limit.d.ts +10 -0
  274. package/dist/observability/rate-limit.js +1 -0
  275. package/dist/observability/tool-trace.d.ts +24 -0
  276. package/dist/observability/tool-trace.js +41 -1
  277. package/dist/orchestration/hardened-vm-runner.d.ts +107 -0
  278. package/dist/orchestration/hardened-vm-runner.js +119 -4
  279. package/dist/orchestration/hardened-vm-worker-runner.d.ts +14 -0
  280. package/dist/orchestration/hardened-vm-worker-runner.js +26 -1
  281. package/dist/orchestration/hardened-vm-worker.js +27 -0
  282. package/dist/orchestration/subagent-steer.d.ts +44 -0
  283. package/dist/orchestration/subagent-steer.js +45 -1
  284. package/dist/orchestration/workflow-agent-steer.d.ts +54 -0
  285. package/dist/orchestration/workflow-agent-steer.js +80 -1
  286. package/dist/orchestration/workflow-completion-inbox.d.ts +189 -2
  287. package/dist/orchestration/workflow-completion-inbox.js +283 -32
  288. package/dist/orchestration/workflow-notify-journal.d.ts +132 -1
  289. package/dist/orchestration/workflow-notify-journal.js +187 -13
  290. package/dist/parked-decide.d.ts +61 -0
  291. package/dist/parked-decide.js +58 -1
  292. package/dist/per-task-image.d.ts +54 -0
  293. package/dist/per-task-image.js +41 -0
  294. package/dist/plan-cache-probe.d.ts +35 -0
  295. package/dist/plan-cache-probe.js +18 -3
  296. package/dist/plugins/approval-exemption-store.d.ts +7 -0
  297. package/dist/plugins/approval-exemption-store.js +28 -2
  298. package/dist/plugins/approval-store-sql.d.ts +64 -0
  299. package/dist/plugins/approval-store-sql.js +32 -0
  300. package/dist/plugins/background-agent-store-sql.d.ts +55 -0
  301. package/dist/plugins/background-agent-store-sql.js +91 -4
  302. package/dist/plugins/background-shell-support.d.ts +98 -0
  303. package/dist/plugins/background-shell-support.js +117 -13
  304. package/dist/plugins/blob-backend.d.ts +104 -0
  305. package/dist/plugins/blob-backend.js +163 -6
  306. package/dist/plugins/breaker-state-sql.d.ts +65 -1
  307. package/dist/plugins/breaker-state-sql.js +46 -8
  308. package/dist/plugins/caching-session-store.d.ts +42 -0
  309. package/dist/plugins/caching-session-store.js +63 -3
  310. package/dist/plugins/checkpoint-store-sql.d.ts +179 -0
  311. package/dist/plugins/checkpoint-store-sql.js +239 -11
  312. package/dist/plugins/file-outcome-sink.d.ts +4 -0
  313. package/dist/plugins/file-outcome-sink.js +9 -0
  314. package/dist/plugins/file-resume-anchor-store.d.ts +20 -0
  315. package/dist/plugins/file-resume-anchor-store.js +43 -4
  316. package/dist/plugins/file-run-store.d.ts +90 -0
  317. package/dist/plugins/file-run-store.js +145 -13
  318. package/dist/plugins/file-snapshot-store-sql.d.ts +133 -1
  319. package/dist/plugins/file-snapshot-store-sql.js +178 -10
  320. package/dist/plugins/file-workflow-journal-store.d.ts +10 -0
  321. package/dist/plugins/file-workflow-journal-store.js +10 -0
  322. package/dist/plugins/fork-routing-session-store.d.ts +47 -0
  323. package/dist/plugins/fork-routing-session-store.js +51 -3
  324. package/dist/plugins/host-platform.d.ts +69 -0
  325. package/dist/plugins/host-platform.js +81 -2
  326. package/dist/plugins/image-bake-store-sql.d.ts +136 -1
  327. package/dist/plugins/image-bake-store-sql.js +241 -7
  328. package/dist/plugins/image-index-sql.d.ts +46 -0
  329. package/dist/plugins/image-index-sql.js +113 -4
  330. package/dist/plugins/k8s-bg-scripts.d.ts +34 -0
  331. package/dist/plugins/k8s-bg-scripts.js +88 -4
  332. package/dist/plugins/k8s-exec-protocol.d.ts +33 -0
  333. package/dist/plugins/k8s-exec-protocol.js +41 -0
  334. package/dist/plugins/local-checkpoint-store.d.ts +40 -0
  335. package/dist/plugins/local-checkpoint-store.js +86 -7
  336. package/dist/plugins/local-session-store.d.ts +157 -0
  337. package/dist/plugins/local-session-store.js +289 -34
  338. package/dist/plugins/local-task-attachment-store.js +14 -2
  339. package/dist/plugins/mailbox-store-sql.d.ts +38 -0
  340. package/dist/plugins/mailbox-store-sql.js +39 -6
  341. package/dist/plugins/memory-engine-pg.d.ts +30 -0
  342. package/dist/plugins/memory-engine-pg.js +122 -4
  343. package/dist/plugins/memory-engine-tidb.d.ts +26 -0
  344. package/dist/plugins/memory-engine-tidb.js +115 -5
  345. package/dist/plugins/memory-engine-vector-util.d.ts +5 -0
  346. package/dist/plugins/memory-engine-vector-util.js +5 -0
  347. package/dist/plugins/memory-resume-anchor-store.d.ts +8 -0
  348. package/dist/plugins/memory-resume-anchor-store.js +17 -0
  349. package/dist/plugins/memory-run-store.d.ts +63 -0
  350. package/dist/plugins/memory-run-store.js +73 -8
  351. package/dist/plugins/memory-session-policy-store.d.ts +17 -0
  352. package/dist/plugins/memory-session-policy-store.js +17 -0
  353. package/dist/plugins/memory-sync-store-pg.d.ts +42 -0
  354. package/dist/plugins/memory-sync-store-pg.js +45 -2
  355. package/dist/plugins/memory-sync-store-tidb.d.ts +9 -0
  356. package/dist/plugins/memory-sync-store-tidb.js +31 -1
  357. package/dist/plugins/outcome-ledger-sql.d.ts +63 -0
  358. package/dist/plugins/outcome-ledger-sql.js +97 -3
  359. package/dist/plugins/pg-approval-store.d.ts +7 -0
  360. package/dist/plugins/pg-approval-store.js +7 -0
  361. package/dist/plugins/pg-breaker-state.d.ts +6 -0
  362. package/dist/plugins/pg-breaker-state.js +6 -0
  363. package/dist/plugins/pg-checkpoint-store.d.ts +8 -0
  364. package/dist/plugins/pg-checkpoint-store.js +8 -0
  365. package/dist/plugins/pg-cost-quota.d.ts +17 -0
  366. package/dist/plugins/pg-cost-quota.js +6 -0
  367. package/dist/plugins/pg-file-snapshot-store.d.ts +6 -0
  368. package/dist/plugins/pg-file-snapshot-store.js +6 -0
  369. package/dist/plugins/pg-image-bake.d.ts +9 -0
  370. package/dist/plugins/pg-image-bake.js +9 -0
  371. package/dist/plugins/pg-image-index.d.ts +9 -0
  372. package/dist/plugins/pg-image-index.js +9 -0
  373. package/dist/plugins/pg-outcome-ledger.d.ts +9 -0
  374. package/dist/plugins/pg-outcome-ledger.js +9 -0
  375. package/dist/plugins/pg-pool.d.ts +50 -0
  376. package/dist/plugins/pg-pool.js +78 -0
  377. package/dist/plugins/pg-query.d.ts +11 -0
  378. package/dist/plugins/pg-rate-limiter.d.ts +23 -0
  379. package/dist/plugins/pg-rate-limiter.js +12 -2
  380. package/dist/plugins/pg-resume-anchor-store.d.ts +5 -0
  381. package/dist/plugins/pg-resume-anchor-store.js +5 -0
  382. package/dist/plugins/pg-run-store.d.ts +7 -0
  383. package/dist/plugins/pg-run-store.js +7 -0
  384. package/dist/plugins/pg-safe-json.d.ts +13 -0
  385. package/dist/plugins/pg-safe-json.js +40 -4
  386. package/dist/plugins/pg-session-policy-store.d.ts +5 -0
  387. package/dist/plugins/pg-session-policy-store.js +5 -0
  388. package/dist/plugins/pg-session-storage.d.ts +72 -0
  389. package/dist/plugins/pg-session-storage.js +152 -20
  390. package/dist/plugins/pg-session-store.d.ts +10 -0
  391. package/dist/plugins/pg-session-store.js +10 -0
  392. package/dist/plugins/pg-tool-result-store.d.ts +7 -0
  393. package/dist/plugins/pg-tool-result-store.js +7 -0
  394. package/dist/plugins/pg-workflow-journal-store.d.ts +7 -0
  395. package/dist/plugins/pg-workflow-journal-store.js +7 -0
  396. package/dist/plugins/pg-workflow-run-store.d.ts +7 -0
  397. package/dist/plugins/pg-workflow-run-store.js +7 -0
  398. package/dist/plugins/posix-shell-fs.d.ts +3 -0
  399. package/dist/plugins/posix-shell-fs.js +31 -1
  400. package/dist/plugins/remote-env-adb.d.ts +25 -0
  401. package/dist/plugins/remote-env-adb.js +95 -12
  402. package/dist/plugins/remote-env-e2b.d.ts +146 -0
  403. package/dist/plugins/remote-env-e2b.js +371 -40
  404. package/dist/plugins/remote-env-file-error.d.ts +34 -0
  405. package/dist/plugins/remote-env-file-error.js +37 -0
  406. package/dist/plugins/remote-env-host.d.ts +151 -0
  407. package/dist/plugins/remote-env-host.js +468 -70
  408. package/dist/plugins/remote-env-k8s.d.ts +135 -0
  409. package/dist/plugins/remote-env-k8s.js +318 -32
  410. package/dist/plugins/remote-env-local-docker.d.ts +65 -0
  411. package/dist/plugins/remote-env-local-docker.js +149 -24
  412. package/dist/plugins/remote-env-ssh.d.ts +35 -0
  413. package/dist/plugins/remote-env-ssh.js +117 -20
  414. package/dist/plugins/remote-scratchpad.d.ts +38 -0
  415. package/dist/plugins/remote-scratchpad.js +32 -2
  416. package/dist/plugins/remote-shell.d.ts +22 -0
  417. package/dist/plugins/remote-shell.js +27 -1
  418. package/dist/plugins/resume-anchor-store-sql.d.ts +28 -0
  419. package/dist/plugins/resume-anchor-store-sql.js +11 -0
  420. package/dist/plugins/roster-store-sql.d.ts +27 -0
  421. package/dist/plugins/roster-store-sql.js +54 -0
  422. package/dist/plugins/run-store-sql.d.ts +194 -0
  423. package/dist/plugins/run-store-sql.js +210 -9
  424. package/dist/plugins/s3-presign.d.ts +20 -0
  425. package/dist/plugins/s3-presign.js +49 -2
  426. package/dist/plugins/scheduler-support.d.ts +12 -0
  427. package/dist/plugins/scheduler-support.js +75 -3
  428. package/dist/plugins/send-file-ledger.d.ts +15 -0
  429. package/dist/plugins/send-file-ledger.js +53 -4
  430. package/dist/plugins/send-user-file.d.ts +77 -0
  431. package/dist/plugins/send-user-file.js +92 -4
  432. package/dist/plugins/session-policy-store-sql.d.ts +15 -0
  433. package/dist/plugins/session-policy-store-sql.js +75 -3
  434. package/dist/plugins/session-store.d.ts +45 -0
  435. package/dist/plugins/session-store.js +51 -0
  436. package/dist/plugins/sql-driver.d.ts +77 -0
  437. package/dist/plugins/sql-driver.js +4 -0
  438. package/dist/plugins/sql-escape.d.ts +8 -0
  439. package/dist/plugins/sql-escape.js +8 -0
  440. package/dist/plugins/sql-row-helpers.d.ts +25 -0
  441. package/dist/plugins/sql-row-helpers.js +25 -0
  442. package/dist/plugins/store-backend.d.ts +103 -0
  443. package/dist/plugins/store-backend.js +120 -27
  444. package/dist/plugins/store-contracts.d.ts +62 -0
  445. package/dist/plugins/store-contracts.js +3 -0
  446. package/dist/plugins/task-attachment-store.d.ts +60 -0
  447. package/dist/plugins/task-attachment-store.js +49 -3
  448. package/dist/plugins/task-list-store-sql.d.ts +25 -0
  449. package/dist/plugins/task-list-store-sql.js +21 -0
  450. package/dist/plugins/tidb-approval-store.d.ts +6 -0
  451. package/dist/plugins/tidb-approval-store.js +6 -0
  452. package/dist/plugins/tidb-breaker-state.d.ts +5 -0
  453. package/dist/plugins/tidb-breaker-state.js +5 -0
  454. package/dist/plugins/tidb-checkpoint-store.d.ts +7 -0
  455. package/dist/plugins/tidb-checkpoint-store.js +7 -0
  456. package/dist/plugins/tidb-cost-quota.d.ts +6 -0
  457. package/dist/plugins/tidb-cost-quota.js +1 -0
  458. package/dist/plugins/tidb-file-snapshot-store.d.ts +6 -0
  459. package/dist/plugins/tidb-file-snapshot-store.js +6 -0
  460. package/dist/plugins/tidb-image-bake.d.ts +8 -0
  461. package/dist/plugins/tidb-image-bake.js +8 -0
  462. package/dist/plugins/tidb-image-index.d.ts +8 -0
  463. package/dist/plugins/tidb-image-index.js +8 -0
  464. package/dist/plugins/tidb-outcome-ledger.d.ts +10 -0
  465. package/dist/plugins/tidb-outcome-ledger.js +10 -0
  466. package/dist/plugins/tidb-pool.d.ts +56 -0
  467. package/dist/plugins/tidb-pool.js +181 -1
  468. package/dist/plugins/tidb-rate-limiter.d.ts +11 -0
  469. package/dist/plugins/tidb-rate-limiter.js +5 -2
  470. package/dist/plugins/tidb-resume-anchor-store.d.ts +5 -0
  471. package/dist/plugins/tidb-resume-anchor-store.js +5 -0
  472. package/dist/plugins/tidb-run-store.d.ts +7 -0
  473. package/dist/plugins/tidb-run-store.js +7 -0
  474. package/dist/plugins/tidb-session-policy-store.d.ts +5 -0
  475. package/dist/plugins/tidb-session-policy-store.js +5 -0
  476. package/dist/plugins/tidb-session-storage.d.ts +43 -0
  477. package/dist/plugins/tidb-session-storage.js +62 -3
  478. package/dist/plugins/tidb-session-store.d.ts +142 -0
  479. package/dist/plugins/tidb-session-store.js +264 -16
  480. package/dist/plugins/tidb-tool-result-store.d.ts +5 -0
  481. package/dist/plugins/tidb-tool-result-store.js +7 -0
  482. package/dist/plugins/tidb-workflow-journal-store.d.ts +7 -0
  483. package/dist/plugins/tidb-workflow-journal-store.js +7 -0
  484. package/dist/plugins/tidb-workflow-run-store.d.ts +8 -0
  485. package/dist/plugins/tidb-workflow-run-store.js +8 -0
  486. package/dist/plugins/tool-result-store-sql.d.ts +58 -0
  487. package/dist/plugins/tool-result-store-sql.js +38 -2
  488. package/dist/plugins/web-search.d.ts +35 -0
  489. package/dist/plugins/web-search.js +45 -9
  490. package/dist/plugins/workflow-journal-limits.d.ts +10 -0
  491. package/dist/plugins/workflow-journal-limits.js +10 -0
  492. package/dist/plugins/workflow-journal-store-sql.d.ts +78 -0
  493. package/dist/plugins/workflow-journal-store-sql.js +58 -2
  494. package/dist/plugins/workflow-run-store-sql.d.ts +84 -0
  495. package/dist/plugins/workflow-run-store-sql.js +84 -6
  496. package/dist/plugins/worktree-isolation.d.ts +45 -0
  497. package/dist/plugins/worktree-isolation.js +105 -6
  498. package/dist/plugins/write-behind-counter.d.ts +56 -1
  499. package/dist/plugins/write-behind-counter.js +53 -14
  500. package/dist/principal-jwt.d.ts +24 -0
  501. package/dist/principal-jwt.js +60 -5
  502. package/dist/project-memory.d.ts +22 -0
  503. package/dist/project-memory.js +144 -15
  504. package/dist/prompts-domain-validate.d.ts +28 -0
  505. package/dist/prompts-domain-validate.js +42 -1
  506. package/dist/question.d.ts +33 -0
  507. package/dist/question.js +69 -2
  508. package/dist/resource-suspend.d.ts +21 -0
  509. package/dist/resource-suspend.js +16 -0
  510. package/dist/router/route-orchestration.d.ts +78 -0
  511. package/dist/router/route-orchestration.js +77 -0
  512. package/dist/run-local.d.ts +32 -0
  513. package/dist/run-local.js +172 -8
  514. package/dist/runs.d.ts +189 -4
  515. package/dist/runs.js +465 -24
  516. package/dist/runtime-caps-resolver.d.ts +106 -0
  517. package/dist/runtime-caps-resolver.js +89 -4
  518. package/dist/runtime-governance.d.ts +84 -0
  519. package/dist/runtime-governance.js +127 -1
  520. package/dist/sandbox-pkg-source.d.ts +23 -0
  521. package/dist/sandbox-pkg-source.js +37 -0
  522. package/dist/sealed-key.d.ts +54 -0
  523. package/dist/sealed-key.js +68 -3
  524. package/dist/security.d.ts +108 -0
  525. package/dist/security.js +145 -6
  526. package/dist/sema-registry.d.ts +21 -0
  527. package/dist/sema-registry.js +21 -0
  528. package/dist/session-leaf-bus.d.ts +31 -0
  529. package/dist/session-leaf-bus.js +32 -0
  530. package/dist/session-sync-content.d.ts +46 -0
  531. package/dist/session-sync-content.js +47 -1
  532. package/dist/session-sync-kernel.d.ts +89 -0
  533. package/dist/session-sync-kernel.js +48 -3
  534. package/dist/session-sync.d.ts +125 -0
  535. package/dist/session-sync.js +181 -8
  536. package/dist/session-titler.d.ts +26 -0
  537. package/dist/session-titler.js +41 -7
  538. package/dist/session-watch.d.ts +53 -1
  539. package/dist/session-watch.js +73 -9
  540. package/dist/sighup-idle.d.ts +30 -0
  541. package/dist/sighup-idle.js +12 -1
  542. package/dist/spec-fields.d.ts +91 -3
  543. package/dist/spec-fields.js +116 -4
  544. package/dist/task-cwd.d.ts +50 -0
  545. package/dist/task-cwd.js +68 -1
  546. package/dist/task-mcp.d.ts +47 -0
  547. package/dist/task-mcp.js +29 -0
  548. package/dist/task-settings.d.ts +175 -0
  549. package/dist/task-settings.js +226 -11
  550. package/dist/task-workflow.d.ts +67 -0
  551. package/dist/task-workflow.js +75 -8
  552. package/dist/tool-approval.d.ts +91 -0
  553. package/dist/tool-approval.js +215 -0
  554. package/dist/trace/artifacts.d.ts +9 -0
  555. package/dist/trace/artifacts.js +30 -5
  556. package/dist/trace/core-keyset-guard.d.ts +18 -0
  557. package/dist/trace/ledger-sink.d.ts +43 -0
  558. package/dist/trace/ledger-sink.js +64 -5
  559. package/dist/trace/project.d.ts +133 -0
  560. package/dist/trace/project.js +189 -2
  561. package/dist/trace/redact.d.ts +19 -0
  562. package/dist/trace/redact.js +66 -9
  563. package/dist/usage-analytics.d.ts +26 -0
  564. package/dist/usage-analytics.js +26 -3
  565. package/package.json +1 -1
@@ -4,9 +4,13 @@ import { redactedPreview } from "../../trace/redact.js";
4
4
  import { sleep } from "../sse-log.js";
5
5
  import { sendJson, sendError, sseHeaders } from "../send.js";
6
6
  import { gatedPrincipal, explicitOperatorOk, isOperator, explicitOperator } from "../principal-gate.js";
7
+ // design/80 seam #2 (assistant-scheduler): graceful preempt (durable yield) + resource_limit resume of one task.
7
8
  export const ASSISTANT_PREEMPT_RE = /^\/v1\/assistant\/tasks\/([^/]+)\/preempt$/;
8
9
  export const ASSISTANT_RESUME_RE = /^\/v1\/assistant\/tasks\/([^/]+)\/resume$/;
9
10
  export const ASSISTANT_PLAN_REVIEW_RE = /^\/v1\/assistant\/tasks\/([^/]+)\/plan_review$/;
11
+ /** Structural check for an operator-supplied QuestionAnswer (durable ask resume, TC-5.4). Strict on the
12
+ * load-bearing shape — `answers[]` non-empty, each `{header: string, selected: string[], note?: string}` —
13
+ * so a typo'd payload fails the request instead of resuming the task with an answer the tool can't use. */
10
14
  export function isQuestionAnswer(v) {
11
15
  if (!v || typeof v !== "object" || Array.isArray(v))
12
16
  return false;
@@ -29,15 +33,28 @@ export async function handleApprovalsAssistant(req, res, url, ctx) {
29
33
  await handleApprovalsAssistantBody(req, res, url, ctx, miss);
30
34
  return !miss.fell;
31
35
  }
36
+ /** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。裸 `return;`
37
+ * = 「本域已应答」;走到函数尾才是「没匹配上」。跨域的东西一律在首行解构出来。 */
32
38
  async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
33
39
  const { deps } = ctx;
34
40
  const { preemptableRuns } = ctx.registry;
35
41
  const { readJson, rateLimited, quotaExceeded } = ctx.helpers;
36
42
  const { resumeCheckpoint, resumePreempted, resumePlanReview } = ctx.legs;
43
+ // Durable F4 (design/45): the approval queue over the CHECKPOINT store. Operators list pending (non-secret)
44
+ // and decide by the session handle → the service maps session→token INTERNALLY and resumes (token never in
45
+ // a URL/body/log). Takes precedence over the poll gate when a checkpointStore is wired.
37
46
  if (deps.checkpointStore && (url.startsWith("/v1/approvals") || url.startsWith("/v1/assistant"))) {
38
47
  const cs = deps.checkpointStore;
39
- const principal = gatedPrincipal(req, deps.config);
48
+ // 🔴 D-G §6: a direct-mode worker NEVER trusts x-agent-principal NOT EVEN for the READ paths (else a forged
49
+ // operator header leaks every tenant's pending tool-call args via GET/stream, adversarial HIGH). For reads there
50
+ // is no per-decision checkpointToken, so identity comes from a verified principal JWT WITHOUT a cnf.bnd; an
51
+ // absent/invalid token ⇒ undefined principal ⇒ scope "__none__" + operator=false (fail-closed). The POST /decide
52
+ // path below re-verifies the SAME token WITH cnf.bnd (channel-bound to the action).
53
+ const principal = gatedPrincipal(req, deps.config); // direct-door safe — identity-only proof; single source of truth (see gatedPrincipal)
40
54
  const operator = isOperator(principal, deps.config.operatorPrincipals);
55
+ // design/80 (native push): SSE stream of pending-approval deltas so the portal SUBSCRIBES ONCE instead of
56
+ // polling GET /v1/approvals every ~10s. Same operator/owner scope as the list. Must precede the list/decide
57
+ // matches (the decide regex would otherwise treat "stream" as a sessionId on a POST — but this is GET-only).
41
58
  if (req.method === "GET" && url === "/v1/approvals/stream") {
42
59
  const scope = operator
43
60
  ? (new URL(req.url ?? "", "http://x").searchParams.get("owner") ?? undefined)
@@ -47,11 +64,15 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
47
64
  }
48
65
  if (req.method === "GET" && url === "/v1/approvals") {
49
66
  const scope = operator
50
- ? (new URL(req.url ?? "", "http://x").searchParams.get("owner") ?? undefined)
51
- : (principal ?? "__none__");
67
+ ? (new URL(req.url ?? "", "http://x").searchParams.get("owner") ?? undefined) // operator: all (or ?owner)
68
+ : (principal ?? "__none__"); // non-operator: only its own scope (never others' pending)
52
69
  sendJson(res, 200, { pending: await cs.listPending(scope) });
53
70
  return;
54
71
  }
72
+ // exemptions surface — the UI's "本会话不再询问" state (list) + revoke. Same authz shape as
73
+ // /decide: an explicit operator may inspect any session; otherwise the caller must OWN the session's scope
74
+ // (peekPendingScope only knows PENDING sessions, so ownership here reads the session store's owner — the
75
+ // durable authority). Non-owner ⇒ 404 (no existence oracle, decide-route parity).
55
76
  {
56
77
  const em = /^\/v1\/approvals\/([^/]+)\/exemptions(?:\/([^/]+))?$/.exec(url);
57
78
  if (em && (req.method === "GET" || req.method === "DELETE")) {
@@ -72,6 +93,12 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
72
93
  const operators = deps.config.operatorPrincipals;
73
94
  const isExplicitOperator = operators.length > 0 && principal !== undefined && operators.includes(principal);
74
95
  if (!isExplicitOperator) {
96
+ // 🔴 fail-CLOSED owner gate (both reviewers, 2026-07-13): mirror the sibling owner-gated routes
97
+ // (session policy/delete) — ownerOf REQUIRED (absent ⇒ 501, never open), a store error PROPAGATES
98
+ // (500, never open; the old `.catch(() => undefined)` flipped a DB blip into authorization), and an
99
+ // UNKNOWN session (owner === undefined) 404s with no existence oracle. owner === null (ownerless
100
+ // anonymous/dev session) stays open — decide-route parity. No uuid shape-gate: this population is
101
+ // the checkpoint sessionId space (decide-route parity), not the sessions-API surface.
75
102
  const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
76
103
  if (!ownerOf) {
77
104
  sendError(res, 501, "capability.session_store_required", "approval exemptions require a session-store backend");
@@ -85,7 +112,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
85
112
  }
86
113
  if (req.method === "GET") {
87
114
  if (exToolName !== undefined) {
88
- sendError(res, 404, "request.route_unsupported", "not found");
115
+ sendError(res, 404, "request.route_unsupported", "not found"); // GET is list-only (no per-tool read)
89
116
  return;
90
117
  }
91
118
  sendJson(res, 200, { exemptions: await deps.approvalExemptionStore.list(exSessionId) });
@@ -100,30 +127,67 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
100
127
  return;
101
128
  }
102
129
  }
130
+ // design/80 #2 (assistant-scheduler, design/75 §3): the SINGLE INBOX — one prioritized view of the HITL gates
131
+ // across ALL of a user's N concurrent durable tasks. = listPending(scope) (already severity-sorted with the
132
+ // riskDescriptor) ENRICHED with each task's objective so the user knows WHICH of their tasks each gate belongs
133
+ // to (the missing cross-task aggregation). Scheduling priority = the severity sort; preemption is
134
+ // a later policy layer. Same owner-scoping as listPending (a non-operator sees only its own tasks' gates).
103
135
  if (req.method === "GET" && url === "/v1/assistant/inbox") {
136
+ // SECURITY (review w16yqkkxv): the assistant inbox is a PER-USER view — ALWAYS scope to a CONCRETE owner,
137
+ // never a system-wide dump. A non-operator sees its own principal; an operator may inspect ONE owner via
138
+ // ?owner, else its own. (≠ /v1/approvals, the admin queue, which intentionally lists all.) This removes the
139
+ // scope=undefined / empty-?owner path that made listPending() return every tenant's pendings unbounded.
104
140
  const ownerParam = new URL(req.url ?? "", "http://x").searchParams.get("owner") || undefined;
105
141
  const scope = operator && ownerParam ? ownerParam : (principal ?? "__none__");
142
+ // 🔴 (BUG FIX) the inbox is the documented CheckpointSummary view (gateKind + severity SCALAR +
143
+ // spentMicroUsd + deadline), NOT the rich operator-queue PendingCheckpoint. It USED to spread listPending()'s
144
+ // raw rich rows — which carried `riskDescriptor.severity` (an object, not the scalar), the tool input + the
145
+ // decide binding, and NO gateKind — diverging from the contract + the tasks view (which already projects via
146
+ // summarizeCheckpoint). Switched to listByScope (the SAME summarizeCheckpoint projection tasks uses) so the
147
+ // inbox + tasks are consistent; the rich decide-binding lives on /v1/approvals (the operator queue), not here.
106
148
  const summaries = await cs.listByScope(scope);
107
149
  const inbox = (await Promise.all(summaries.map(async (s) => {
150
+ // `createdAt` + `toolInput` are now PROJECTED into CheckpointSummary by core's summarizeCheckpoint
151
+ // (core 1.116.0): listByScope returns them in
152
+ // ONE query, so the per-entry `cs.get(token)` N+1 is GONE. `objective` lives in the task body
153
+ // (checkpoint_ctx), not the summary, so it still needs getCtx (best-effort; missing ⇒ null).
108
154
  const ctx = await cs.getCtx(s.sessionId).catch(() => null);
155
+ // 🔴 STRIP the resume `token` (the unexposed secret capability — resume is by sessionId, never the
156
+ // token) AND the RAW `toolInput` (core bounds but does NOT redact it — redaction is
157
+ // the consumer's job). Re-emit the REDACTED `input` instead.
109
158
  const { token: _token, toolInput: _rawToolInput, ...safe } = s;
159
+ // `input` = a REDACTED preview of the projected (bounded, RAW) tool_input. Only `tool_approval` gates
160
+ // carry it (resource_limit / plan_review have none ⇒ toolInput undefined ⇒ input null). NOT the old
161
+ // bug (raw rich row + decide-binding); the decide-binding still lives only on /v1/approvals.
110
162
  const input = s.toolInput != null ? redactedPreview(s.toolInput) : null;
111
163
  return { ...safe, objective: ctx?.body?.objective ?? null, input };
112
- }))).sort((a, b) => (b.severity ?? 0) - (a.severity ?? 0));
113
- sendJson(res, 200, { inbox });
164
+ }))).sort((a, b) => (b.severity ?? 0) - (a.severity ?? 0)); // severity DESC (listByScope is created_at ASC; stable → oldest-first within a tier)
165
+ sendJson(res, 200, { inbox }); // CheckpointSummary (token-stripped) + objective, severity-prioritized — the assistant single inbox
114
166
  return;
115
167
  }
168
+ // design/80 #2 (assistant-scheduler): the N-TASKS overview = the cross-task SCHEDULING view. A user's active
169
+ // durable agent tasks, each joined (when suspended on a HITL gate) with the core scheduler seam
170
+ // listByScope(scope) → {gateKind, severity, spentMicroUsd, deadline} (core 1.105, ONE query, no N+1). The
171
+ // result is ordered for triage: blocked-on-a-gate first, then by risk severity DESC, then by budget spent
172
+ // DESC — i.e. the order a scheduler would advance/escalate them. Same owner-scoping. Needs the run-store.
116
173
  if (req.method === "GET" && url === "/v1/assistant/tasks") {
174
+ // SECURITY (review w16yqkkxv): per-user view — ALWAYS a CONCRETE owner (operator inspects ONE via ?owner,
175
+ // else its own; non-operator = its own). No scope=undefined → no unbounded cross-tenant listPending()/listRuns.
117
176
  const ownerParam = new URL(req.url ?? "", "http://x").searchParams.get("owner") || undefined;
118
177
  const scope = operator && ownerParam ? ownerParam : (principal ?? "__none__");
119
178
  if (!deps.runStore) {
120
179
  sendJson(res, 200, { tasks: [] });
121
180
  return;
122
181
  }
182
+ // Rich per-gate summary via the core seam (severity + budget) — ONE scoped query, bounded (LIMIT 500).
123
183
  const summaries = await cs.listByScope(scope);
124
184
  const byGate = new Map(summaries.map((s) => [s.sessionId, s]));
125
185
  const runs = await deps.runStore.listRuns({ owner: scope, limit: 100 });
126
186
  const tasks = runs
187
+ // 🔴 wlzq75oac HIGH: a plan_review / dry-run review park persists as status='needs_review' (NOT 'suspended'),
188
+ // so a running/suspended-only filter DROPPED exactly the HITL-gated tasks §3 promises to surface "needs-
189
+ // attention first" — the operator could never discover the very plan_review tasks §4c says to resolve.
190
+ // Same class as the inbox leak (handler vs contract drift). needs_review is a non-terminal park → include it.
127
191
  .filter((r) => r.status === "running" || r.status === "suspended" || r.status === "needs_review")
128
192
  .map((r) => {
129
193
  const s = byGate.get(r.sessionId);
@@ -134,18 +198,32 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
134
198
  sendJson(res, 200, { tasks });
135
199
  return;
136
200
  }
201
+ // design/80 seam #2: POST /v1/assistant/tasks/:id/preempt — the scheduler's graceful "yield this task"
202
+ // (durable + resumable; the OPPOSITE of /v1/runs/:id/cancel's kill). Durable cross-replica preempt flag
203
+ // (the owning instance's heartbeat honors it) + a same-replica preempt-controller fast path. On a direct
204
+ // worker the caller is the crypto-VERIFIED JWT principal (above) — owner-scoped, no trusted-header bypass.
137
205
  const preemptMatch = req.method === "POST" ? ASSISTANT_PREEMPT_RE.exec(url) : null;
138
206
  if (preemptMatch) {
139
207
  if (rateLimited(req, res) || quotaExceeded(req, res))
140
- return;
208
+ return; // 🔴 复审 C3:preempt is NON-billable
209
+ // (yields a task, runs no model — see the billable-route classifier) → NO lease gate, else an exhausted
210
+ // tenant could not stop its own spending.
141
211
  if (!deps.runStore) {
142
212
  sendError(res, 501, "capability.run_store_required", "preemption requires the TiDB run store");
143
213
  return;
144
214
  }
215
+ // requirePrincipal parity with the sibling mutating endpoints (runOwnerOk / cancel / runs): 401 before the
216
+ // lookup when configured (uses the crypto-verified `principal` — never the trusted header on a direct door).
145
217
  if (deps.config.requirePrincipal && principal === undefined) {
146
218
  sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
147
219
  return;
148
220
  }
221
+ // Precondition: a preempt only durably suspends when the worker opts tasks into resourceSuspend
222
+ // (RESOURCE_SUSPEND=true) AND has the durable-suspend substrate (a checkpoint store — resolveSpec wires
223
+ // resourceSuspend ONLY inside the durableEnabled block, so without a checkpoint store the preemptSignal is
224
+ // never armed) AND core deems the task eligible (remote env + durable tool-results). Reject honestly when the
225
+ // worker can't preempt at all (e.g. the local in-memory backend has no checkpoint store), rather than 202
226
+ // "preempting" a signal nothing honors.
149
227
  if (!deps.config.resourceSuspend || !deps.checkpointStore) {
150
228
  sendError(res, 501, "feature.preemption_disabled", "preemption not enabled on this worker (needs RESOURCE_SUSPEND=true + a durable checkpoint store)");
151
229
  return;
@@ -156,6 +234,10 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
156
234
  sendError(res, 404, "not_found.run", "task not found");
157
235
  return;
158
236
  }
237
+ // Owner-gate: an EXPLICIT operator may preempt any tenant's task (preemption is a scheduler action); a
238
+ // normal caller only its own. 404 (not 403) for a non-owner — no existence oracle (parity with runOwnerOk
239
+ // + the cancel/decide precedent). NOT the bare isOperator (its empty-list "true-for-all" would let any
240
+ // caller preempt anyone — same trap the /decide owner-gate avoids).
159
241
  const operators = deps.config.operatorPrincipals;
160
242
  const explicitOperator = operators.length > 0 && principal !== undefined && operators.includes(principal);
161
243
  if (!explicitOperator && run.owner !== null && run.owner !== principal) {
@@ -163,18 +245,25 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
163
245
  return;
164
246
  }
165
247
  if (run.status === "running") {
248
+ // Honesty guard (review wtyc7ist3): if THIS replica owns the run (instance match) yet it is NOT in
249
+ // preemptableRuns, it is a verify/cascade leg — those durably suspend as a FAILED result, so they are
250
+ // deliberately never registered + never honored (runs.ts). Don't set a dead durable flag or claim
251
+ // "preempting"; report the honest no-op. (Owned-elsewhere → instanceId mismatch → fall through to the
252
+ // durable flag so the owning replica's heartbeat honors it iff that leg is eligible — best-effort.)
166
253
  const ownedHere = run.instanceId != null && run.instanceId === deps.instanceId;
167
254
  if (ownedHere && !preemptableRuns.has(taskId)) {
168
255
  sendJson(res, 202, { taskId, status: run.status, note: "task is not preempt-eligible (verify/cascade leg) — preempt is a no-op" });
169
256
  return;
170
257
  }
171
- const flagged = await deps.runStore.requestPreempt(taskId, run.owner);
258
+ // Durable flag FIRST (only flips a still-running row), abort after — if the run suspended/terminal'd in
259
+ // the getRun→here window the flag affects 0 rows, so re-read and answer by the ACTUAL state.
260
+ const flagged = await deps.runStore.requestPreempt(taskId, run.owner); // owner guard: single-DB defense-in-depth (owner-gate already enforced above)
172
261
  if (!flagged) {
173
262
  const now = await deps.runStore.getRun(taskId);
174
263
  sendJson(res, 202, { taskId, status: now?.status ?? "failed", note: "task no longer running — preempt is a no-op" });
175
264
  return;
176
265
  }
177
- preemptableRuns.get(taskId)?.abort();
266
+ preemptableRuns.get(taskId)?.abort(); // fast path: preempt landed on the running instance → suspend now
178
267
  sendJson(res, 202, { taskId, status: "preempting", note: "graceful durable yield — the task suspends at the next clean turn boundary if preempt-eligible; resume via POST /v1/assistant/tasks/:id/resume" });
179
268
  }
180
269
  else if (run.status === "suspended") {
@@ -185,10 +274,13 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
185
274
  }
186
275
  return;
187
276
  }
277
+ // design/80 seam #2: POST /v1/assistant/tasks/:id/resume — resume a task durably SUSPENDED on a
278
+ // resource_limit gate (a scheduler preempt OR a budget/turns/walltime slice, design/74). NOT an approval
279
+ // gate (resumePreempted's gate-kind guard 409s a policy_ask — that goes through POST /v1/approvals/:id/decide).
188
280
  const resumeMatch = req.method === "POST" ? ASSISTANT_RESUME_RE.exec(url) : null;
189
281
  if (resumeMatch) {
190
282
  if (rateLimited(req, res) || quotaExceeded(req, res))
191
- return;
283
+ return; // 🔴 复审 C2:lease admitted INSIDE driveResumeIntoRunLog on the CHECKPOINT-OWNER principal (the billed tenant), not the request principal — a cross-tenant operator resume must charge the owner's lease, not the operator's. resume hits TiDB + runs the model
192
284
  if (!deps.runStore) {
193
285
  sendError(res, 501, "capability.run_store_required", "resume requires the TiDB run store");
194
286
  return;
@@ -196,7 +288,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
196
288
  if (deps.config.requirePrincipal && principal === undefined) {
197
289
  sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
198
290
  return;
199
- }
291
+ } // parity with sibling mutating endpoints
200
292
  const taskId = resumeMatch[1];
201
293
  const run = await deps.runStore.getRun(taskId);
202
294
  if (!run) {
@@ -209,14 +301,23 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
209
301
  sendError(res, 404, "not_found.run", "task not found");
210
302
  return;
211
303
  }
212
- const out = await resumePreempted(run.sessionId, req);
304
+ const out = await resumePreempted(run.sessionId, req); // gate-kind guard + drive into the run log (shared)
213
305
  sendJson(res, out.status, out.body);
214
306
  return;
215
307
  }
308
+ // design/80 D-B: POST /v1/assistant/tasks/:id/plan_review — resolve a PRE-ACTION `plan_review` gate, 3-state
309
+ // (approve | edit | reject). A SIBLING of preempt/resume (a scheduler-resolvable gate with NO bound tool
310
+ // action — the human reviews the PLAN, not a tool call), NOT the /decide wire (a tool-approval allow/deny +
311
+ // action binding) and NOT the needs_review wire (never reuse it). `edit` carries the
312
+ // operator's REVISED plan (core threads `editedPlan` into the resumed run). 🔴 D-G ruling:
313
+ // a plan_review binds NO action, so on a direct-door worker the self-asserted x-agent-principal is NEVER
314
+ // trusted — it carries the PRINCIPAL-ONLY proof (verifyDirectDoorProof actionBinding:false = JWT identity +
315
+ // cnf.bnd + HMAC over {sessionId,decision}, no boundInputHash), then the SAME operator-or-owner gate on the
316
+ // CRYPTO-VERIFIED principal. On a BFF worker the trusted-header principal owner-gate applies (parity with resume).
216
317
  const planReviewMatch = req.method === "POST" ? ASSISTANT_PLAN_REVIEW_RE.exec(url) : null;
217
318
  if (planReviewMatch) {
218
319
  if (rateLimited(req, res) || quotaExceeded(req, res))
219
- return;
320
+ return; // 🔴 复审 C2:lease admitted in driveResumeIntoRunLog (owner principal). resume hits TiDB + runs the model (approve/edit)
220
321
  if (!deps.runStore) {
221
322
  sendError(res, 501, "capability.run_store_required", "plan_review requires the TiDB run store");
222
323
  return;
@@ -224,8 +325,13 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
224
325
  if (deps.config.requirePrincipal && principal === undefined) {
225
326
  sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
226
327
  return;
227
- }
328
+ } // parity with sibling mutating endpoints
228
329
  const taskId = planReviewMatch[1];
330
+ // 3-state decision; `edit` MUST carry the revised plan, `approve`/`reject` must NOT (a stray editedPlan
331
+ // signals a wrong-gate call — reject, don't drop it). Parsed + validated BEFORE the task is resolved so
332
+ // every bad-body 400 is existence-INDEPENDENT (depends only on the body). 🔴 wq64gmm5e LOW: getRun's 404
333
+ // used to fire BEFORE the body/proof — a task-existence oracle (404-vs-401) on a direct-door worker,
334
+ // divergent from /decide. Now the existence branch comes AFTER and is status-uniform with a bad proof.
229
335
  const body = (await readJson(req));
230
336
  const decision = body?.decision === "approve" ? "approve" : body?.decision === "edit" ? "edit" : body?.decision === "reject" ? "reject" : undefined;
231
337
  if (!decision) {
@@ -244,12 +350,16 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
244
350
  sendError(res, 400, "request.field_invalid", "reason must be a string");
245
351
  return;
246
352
  }
353
+ // 与 /decide 腿同一上限:reason 入签之后,超长只能**拒**(截断产出的字节签名器复现不了 ⇒ 恒 401)。
247
354
  if (typeof body.reason === "string" && body.reason.length > MAX_APPROVAL_REASON_CHARS) {
248
355
  sendError(res, 413, "reason_too_large", `reason too large (max ${MAX_APPROVAL_REASON_CHARS} chars)`);
249
356
  return;
250
357
  }
251
358
  const run = await deps.runStore.getRun(taskId);
252
359
  if (!run) {
360
+ // No such task. On a DIRECT-DOOR worker return the SAME 401 a bad proof gets (parity with /decide): a
361
+ // missing task is indistinguishable from a bad proof — the caller cannot mint a valid proof for an
362
+ // unknown sessionId anyway — so existence never leaks. On a BFF worker the trusted-header owner posture 404s.
253
363
  if (deps.config.directDoorActive) {
254
364
  sendError(res, 401, "principal_unverified", "principal proof required");
255
365
  return;
@@ -257,10 +367,15 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
257
367
  sendError(res, 404, "not_found.run", "task not found");
258
368
  return;
259
369
  }
370
+ // ── D-G principal-only proof (non-action gate): on a direct-door worker derive the CRYPTO-VERIFIED principal
371
+ // from the signed proof, never the self-asserted header. The envelope binds {sessionId, decision} so a forged
372
+ // or replayed plan decision is rejected; no boundInputHash (there is no tool action). fail-closed.
260
373
  let deciderPrincipal = principal;
261
374
  if (deps.config.directDoorActive) {
262
375
  const hdr = (n) => { const h = req.headers[n]; return Array.isArray(h) ? h[0] : h; };
263
- const proof = verifyDirectDoorProof({ jwt: hdr("x-approval-principal-token"), mac: hdr("x-approval-mac"), kid: hdr("x-approval-mac-kid") }, { sessionId: run.sessionId, decision, reason: typeof body.reason === "string" ? body.reason : null }, deps.config, { actionBinding: false });
376
+ const proof = verifyDirectDoorProof({ jwt: hdr("x-approval-principal-token"), mac: hdr("x-approval-mac"), kid: hdr("x-approval-mac-kid") },
377
+ // reason 入签:同一条规则也适用于身份绑定腿(两个绑定位为 null,第五位照常参与)。
378
+ { sessionId: run.sessionId, decision, reason: typeof body.reason === "string" ? body.reason : null }, deps.config, { actionBinding: false });
264
379
  if (!proof.ok) {
265
380
  sendError(res, proof.status, proof.errorCode, proof.error);
266
381
  return;
@@ -272,7 +387,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
272
387
  if (!explicitOperator && run.owner !== null && run.owner !== deciderPrincipal) {
273
388
  sendError(res, 404, "not_found.run", "task not found");
274
389
  return;
275
- }
390
+ } // no existence oracle (verified principal on a direct worker)
276
391
  const out = await resumePlanReview(run.sessionId, decision, decision === "edit" ? body.editedPlan : undefined, typeof body.reason === "string" ? body.reason : undefined, req);
277
392
  sendJson(res, out.status, out.body);
278
393
  return;
@@ -280,10 +395,12 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
280
395
  const m = /^\/v1\/approvals\/([^/]+?)(?:\/decide)?$/.exec(url);
281
396
  if (m && req.method === "POST") {
282
397
  if (rateLimited(req, res) || quotaExceeded(req, res))
283
- return;
398
+ return; // 🔴 复审 C2:lease admitted in driveResumeIntoRunLog (owner principal). resume hits TiDB — rate-limit (council)
399
+ // NOTE: the operator gate is checked AFTER the body parse below — D-G's direct door needs the parsed
400
+ // envelope (checkpointToken + decision) to verify the HMAC before it can derive the deciding principal.
284
401
  let sessionId;
285
402
  try {
286
- sessionId = decodeURIComponent(m[1]);
403
+ sessionId = decodeURIComponent(m[1]); // a malformed %-sequence is a client error (400), not a 500
287
404
  }
288
405
  catch {
289
406
  sendError(res, 400, "request.path_malformed", "malformed approval id (invalid percent-encoding)");
@@ -295,6 +412,11 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
295
412
  sendError(res, 400, "request.body_shape", "body must be { decision: 'approve' | 'deny', reason?, answer?, checkpointToken?, boundCallId?, boundInputHash?, updatedInput?, remember? }");
296
413
  return;
297
414
  }
415
+ // `reason` 现在**在签名载荷里**(三方裁 (a),见 security.ts `approvalHmacMessage` 顶注)⇒ 它的形必须先钉死:
416
+ // · 非字符串会让规范化编码产出一个签名器不可能复现的消息(对象会被内联进 JSON)—— 此前这条腿
417
+ // 根本没校验 reason 的类型(plan_review 腿有),入签之后这就成了硬前置。
418
+ // · 超长**拒而不截**:服务端截过的字节与签名方签的字节必然不同 ⇒ MAC 恒不匹配,截断只会把一个
419
+ // 可诊断的 413 变成费解的 401(与 steer 入参上限同一判据:诚实拒绝优于自作主张改写)。
298
420
  if (body.reason !== undefined && typeof body.reason !== "string") {
299
421
  sendError(res, 400, "request.field_invalid", "reason must be a string");
300
422
  return;
@@ -303,6 +425,11 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
303
425
  sendError(res, 413, "reason_too_large", `reason too large (max ${MAX_APPROVAL_REASON_CHARS} chars)`);
304
426
  return;
305
427
  }
428
+ // `remember:"session"` — record a per-session per-toolName approval exemption alongside an
429
+ // approve ("本会话不再询问"). Closed enum (extensible later, e.g. "principal"); deny+remember = 400 (a
430
+ // "remembered deny" is a different semantic — not smuggled in here); direct-door v1 = 400 (remember is
431
+ // NOT in the HMAC envelope's signed payload — accepting it unsigned would be a tamper hole; extending
432
+ // the MAC payload is a v2 with the BFF, see docs/design/APPROVAL-SESSION-EXEMPTION.md).
306
433
  let remember = false;
307
434
  if (body.remember !== undefined) {
308
435
  if (body.remember !== "session") {
@@ -321,12 +448,20 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
321
448
  sendError(res, 501, "capability.store_required", "approval exemptions need a store backend (DB_BACKEND / LOCAL lane)");
322
449
  return;
323
450
  }
451
+ // 🔴 remember REQUIRES the D-1 decision-binding echo (workflow audit 2026-07-13, my override of the
452
+ // verify-reject): an unbound decide can consume a gate that was SWAPPED after the operator's GET (see
453
+ // gate A, approve lands on gate B) — tolerable as a one-shot legacy quirk, NOT as the mint of a
454
+ // SESSION-PERSISTENT auto-approval for a tool the operator never saw. remember is a NEW contract, so
455
+ // it can demand the strict calling shape (the BFF already receives boundCallId/checkpointToken on the
456
+ // pending row). Plain decide without remember keeps the legacy unbound fallback untouched.
324
457
  if (!body.checkpointToken && !body.boundCallId) {
325
458
  sendError(res, 400, "remember_requires_binding", "remember requires the decision binding (echo checkpointToken and/or boundCallId from the pending approval)");
326
459
  return;
327
460
  }
328
461
  remember = true;
329
462
  }
463
+ // Durable ask (TC-5.4): the operator's out-of-band QuestionAnswer rides the approve. Trusted
464
+ // control-plane input (the model never produces it); malformed = client error, absent = unchanged.
330
465
  let answer;
331
466
  if (body.answer !== undefined) {
332
467
  if (!isQuestionAnswer(body.answer)) {
@@ -335,12 +470,27 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
335
470
  }
336
471
  answer = body.answer;
337
472
  }
473
+ // D-1 (design/80) decision-action binding: the operator echoes back the checkpointToken + boundCallId +
474
+ // boundInputHash they SAW in GET /v1/approvals. The service binds the resume to that exact action (core
475
+ // verifies opaque equality, fail-closed PRE-CAS) and rejects a stale view distinctly (approval_stale).
476
+ // All OPTIONAL — a legacy BFF caller that omits them falls back to the current-pending values (deprecated).
338
477
  for (const f of ["checkpointToken", "boundCallId", "boundInputHash"]) {
339
478
  if (body[f] !== undefined && typeof body[f] !== "string") {
340
479
  sendError(res, 400, "request.field_invalid", `${f} must be a string (the value surfaced on the pending approval record)`);
341
480
  return;
342
481
  }
343
482
  }
483
+ // 🔴 直连门上拒 `updatedInput`(2026-07-26 修),判据与上面拒 `remember` **逐字同一条**:
484
+ // 「不在决策证明的签名载荷里」。此前只 `remember` 有这条门,而 `updatedInput` 改写的是
485
+ // **将被执行的工具实参**(core `runtask.js` 在哈希比对**之后**用 `outcome.updatedInput ?? pendingAction.args`)
486
+ // ⇒ 同一份 JWT+MAC 对「原 body」与「加了 updatedInput 的 body」**都放行**,而 `boundInputHash` 仍是
487
+ // 原动作的哈希。直连门的威胁模型就是「线上字节不可信」(否则不需要 HMAC),能改字节的一方
488
+ // (反代 / 被劫持的客户端)可在操作员批准 `ls` 的同一次决定里换掉实参。
489
+ // ⚠️ 已亲验的缓解:core 会把改写后的实参**重新过一遍 base tool policy**,所以越权面被限制在策略仍允许的
490
+ // 范围内、不是任意命令 —— 但那是"减轻",不是"这个字段被签名覆盖了"。
491
+ // 修法刻意**不是**把它塞进 HMAC 载荷(那要求所有直连门客户端改签名 = 跨仓 wire 变更),而是把**既有的、
492
+ // 代码自己写下的**判据施加到它漏掉的那个字段上:fail-closed、server 单方面可做、与 `remember` 对称。
493
+ // BFF/gated 门(头已由上游验过)**不受影响** —— 这条只在 `directDoorActive` 生效。
344
494
  if (deps.config.directDoorActive && body.updatedInput !== undefined) {
345
495
  sendError(res, 400, "updated_input_not_in_proof", "updatedInput is not supported on a direct-door worker (not covered by the decision proof)");
346
496
  return;
@@ -351,38 +501,69 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
351
501
  boundInputHash: body.boundInputHash,
352
502
  updatedInput: body.updatedInput,
353
503
  };
504
+ // ── design/80 D-G: the authorization gate. On a DIRECT-MODE worker the trusted-header path is DISABLED —
505
+ // every /decide MUST carry the crypto proof (signed principal JWT + cnf.bnd + HMAC envelope), and the
506
+ // self-asserted x-agent-principal is NEVER consulted. On a normal (BFF) worker, the existing trusted-header
507
+ // operator gate is byte-identical. Fail-closed: a direct-door failure rejects, never falls back to the header.
354
508
  let deciderPrincipal = principal;
355
509
  if (deps.config.directDoorActive) {
356
510
  const hdr = (n) => {
357
511
  const h = req.headers[n];
358
512
  return Array.isArray(h) ? h[0] : h;
359
513
  };
360
- const proof = verifyDirectDoorProof({ jwt: hdr("x-approval-principal-token"), mac: hdr("x-approval-mac"), kid: hdr("x-approval-mac-kid") }, { sessionId, boundCallId: binding.boundCallId, boundInputHash: binding.boundInputHash, decision, reason: typeof body.reason === "string" ? body.reason : null }, deps.config);
514
+ const proof = verifyDirectDoorProof({ jwt: hdr("x-approval-principal-token"), mac: hdr("x-approval-mac"), kid: hdr("x-approval-mac-kid") },
515
+ // reason 入签(三方裁 (a) + clay 干净切):落账理由属于决定内容,与 decision 同受完整性保护。
516
+ { sessionId, boundCallId: binding.boundCallId, boundInputHash: binding.boundInputHash, decision, reason: typeof body.reason === "string" ? body.reason : null }, deps.config);
361
517
  if (!proof.ok) {
362
518
  sendError(res, proof.status, proof.errorCode, proof.error);
363
519
  return;
364
520
  }
365
- deciderPrincipal = proof.principal;
521
+ deciderPrincipal = proof.principal; // the CRYPTO-VERIFIED principal replaces the self-asserted header
366
522
  }
523
+ // Authorization (council finding): an EXPLICIT operator may decide ANY approval; otherwise the
524
+ // caller must OWN the checkpoint's scope. 🔴 NOT the bare isOperator — its empty-OPERATOR_PRINCIPALS
525
+ // "true-for-all" would let ANY authenticated caller decide ANOTHER user's checkpoint via a leaked, NON-
526
+ // credential sessionId+boundInputHash (from a log/screenshot/ticket). A non-owner-non-operator gets 404
527
+ // (no existence oracle — parity with runOwnerOk + the cancel P0 precedent), never 403.
367
528
  const operators = deps.config.operatorPrincipals;
368
529
  const explicitOperator = operators.length > 0 && deciderPrincipal !== undefined && operators.includes(deciderPrincipal);
369
530
  if (!explicitOperator) {
370
531
  const cpScope = await cs.peekPendingScope(sessionId);
371
- const ownsIt = cpScope == null || cpScope === "_" || cpScope === deciderPrincipal;
532
+ // "_" = the anonymous-submit scope SENTINEL (main.ts writes auth?.principal ?? "_" at suspend) treat it
533
+ // like null (open), as the sibling sites already do (the 5040/5599/5653 normalizations). Without this an
534
+ // anonymous single-user durable worker DEADLOCKS its own approvals (decide → 404 forever; workflow audit
535
+ // 2026-07-13, confirmed by both verify lenses).
536
+ const ownsIt = cpScope == null || cpScope === "_" || cpScope === deciderPrincipal; // null/"_" = anonymous/dev (open)
372
537
  if (!ownsIt) {
373
538
  sendError(res, 404, "not_found.approval", "approval not found");
374
539
  return;
375
540
  }
376
541
  }
542
+ // remember: capture the pending gate's toolName BEFORE the resume consumes the row. AskUserQuestion
543
+ // gates are excluded (the durable-question policy owns them — an exemption row would be dead weight the
544
+ // ask-gate probe explicitly skips). null toolName (legacy row) ⇒ nothing to remember.
377
545
  let rememberToolName = null;
378
546
  if (remember) {
547
+ // [1.202 live 复验抓获] 旧姿势 `listPending(deciderPrincipal ?? "__none__")` 在匿名单用户 lane
548
+ // (decider 无 principal、checkpoint scope="_" 哨兵)永远滤空 ⇒ remember 结构性失灵(rememberApplied
549
+ // 恒 false,豁免从不落店)。授权已由上方 owner gate 把过(cpScope null/"_"/相符,同 5040/5599/5653
550
+ // 的 "_" 归一先例)——这里直接读该 session 的 pending 行拿 toolName,不再按 decider 二次过滤。
379
551
  const tok = await cs.findPendingTokenBySession(sessionId);
380
552
  const cp0 = tok ? await cs.get(tok) : null;
381
553
  const pa0 = cp0?.pendingAction;
382
554
  const tn = pa0 && pa0.kind === "tool_approval" ? pa0.toolName : null;
383
555
  rememberToolName = tn && tn !== "AskUserQuestion" ? tn : null;
384
556
  }
557
+ // grant timing(codex M2,1.294 isExempt 同腿生效):grant 挪进 onResumeCommitted——markResuming CAS
558
+ // 赢了(decide 已被接受、并发 sibling 已输)之后、模型腿驱动**之前**落店,所以 resumed leg 自己的下一个
559
+ // ask 就能命中豁免(gate isExempt + ask 政策层探针都实时读店);旧姿势 await 整条 resume 腿(可跑数分钟)
560
+ // 之后才 grant,同腿第二次同工具 ask 必再 park 一轮。输掉 CAS(409)/stale(404)⇒ 钩不触发,绝不留
561
+ // 豁免;CAS 赢后模型腿再失败 ⇒ 豁免保留(operator 的 approve+remember 意图在 accept 时已成立,与
562
+ // 「grant 失败不回滚 approve」同向)。grant 失败只 warn 不阻断 resume,响应带 rememberApplied 如实。
385
563
  let rememberApplied = false;
564
+ // overrideSessionId([1591] 候裁② server 修):parked 赎回腿把 grant 锚改到行的 root/host 会话
565
+ // (部署侧豁免探针闭包统一捕获 host 会话键——落子代会话则键永不相交,rememberApplied 会是假话)。
566
+ // legacy 任务腿不传 override,行为逐字不变。
386
567
  const grantOnCommit = remember && deps.approvalExemptionStore && rememberToolName
387
568
  ? async (overrideSessionId) => {
388
569
  const grantSessionId = overrideSessionId ?? sessionId;
@@ -408,8 +589,13 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
408
589
  sendError(res, 404, "not_found.route", "not found");
409
590
  return;
410
591
  }
592
+ // Human-in-the-loop approval gate (F4). Operators list/decide; the running task polls the row.
411
593
  if (deps.approvalStore && url.startsWith("/v1/approvals")) {
412
594
  const store = deps.approvalStore;
595
+ // 🔴 D-G belt (defense-in-depth): this LEGACY block authorizes /approvals via the trusted x-agent-principal
596
+ // header (no crypto). On a direct-door worker that path must NEVER be live — the boot invariant already forces
597
+ // DURABLE_APPROVAL (so checkpointStore owns /approvals above and we never reach here), but if a direct-door
598
+ // worker ever fell through to here, fail closed rather than expose the header-trusting path.
413
599
  if (deps.config.directApprovalDoor) {
414
600
  sendError(res, 503, "approval_door_unconfigured", "approvals are served by the direct door on this worker");
415
601
  return;
@@ -417,10 +603,14 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
417
603
  if (req.method === "GET" && url === "/v1/approvals") {
418
604
  const principal = principalFrom(req, deps.config);
419
605
  if (isOperator(principal, deps.config.operatorPrincipals)) {
606
+ // operator: may filter by any ?owner (or list all). scope is owner-sourced, so the operator's ?owner
607
+ // filter IS the scope filter; an unfiltered operator list is cross-tenant BY DESIGN (operator queue).
420
608
  const owner = new URL(req.url ?? "", "http://x").searchParams.get("owner") ?? undefined;
421
609
  sendJson(res, 200, { pending: owner ? await store.listPending(owner, owner) : await store.listPendingAll() });
422
610
  }
423
611
  else {
612
+ // non-operator: only ITS OWN pending (ignore any ?owner spoof) — never others' tool-call args. scope =
613
+ // the caller's principal (owner-sourced), so the single-DB read guard isolates it at the SQL layer too.
424
614
  sendJson(res, 200, { pending: principal ? await store.listPending(principal, principal) : [] });
425
615
  }
426
616
  return;
@@ -429,12 +619,19 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
429
619
  if (m) {
430
620
  const id = m[1];
431
621
  if (req.method === "GET") {
622
+ // The row carries the full tool-call args (commands/paths/contents of a high-risk op). The list
623
+ // endpoint deliberately scopes non-operators to their OWN pending ("never others' tool-call args");
624
+ // a by-id GET must enforce the same boundary, else any token-holder who learns an id reads another
625
+ // tenant's args. Operators see all; everyone else only their own. 404 (not 403) to non-owners so
626
+ // existence isn't revealed — symmetric with runOwnerOk.
432
627
  const principal = principalFrom(req, deps.config);
433
628
  if (deps.config.requirePrincipal && !principal) {
434
629
  sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
435
630
  return;
436
631
  }
437
632
  const operator = isOperator(principal, deps.config.operatorPrincipals);
633
+ // Operators read across tenants (scope-agnostic getById); a non-operator reads ONLY its own scope so the
634
+ // single-DB read guard fires at the SQL layer (no cross-tenant args even if it learns an id).
438
635
  const row = operator ? await store.getById(id) : principal != null ? await store.get(id, principal) : undefined;
439
636
  if (!row || (!operator && row.owner !== null && row.owner !== principal)) {
440
637
  sendError(res, 404, "not_found.approval", "approval not found");
@@ -444,6 +641,12 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
444
641
  return;
445
642
  }
446
643
  if (req.method === "POST") {
644
+ // Deciding (approve/deny) is OPERATOR-ONLY — a requester approving its own high-risk op would
645
+ // defeat the F4 gate. (Legacy/unset OPERATOR_PRINCIPALS → the service token is the boundary — but
646
+ // ONLY on a single-tenant deployment: under REQUIRE_PRINCIPAL the empty-list true-for-all would let
647
+ // ANY tenant decide ANOTHER tenant's approval [the durable /decide got
648
+ // this hardening earlier and this legacy leg never did] — multi-tenant demands an EXPLICIT
649
+ // operator list, same posture as the durable twin.)
447
650
  const principal = principalFrom(req, deps.config);
448
651
  const operatorOk = deps.config.requirePrincipal
449
652
  ? explicitOperatorOk(gatedPrincipal(req, deps.config), deps.config.operatorPrincipals)
@@ -458,6 +661,9 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
458
661
  sendError(res, 400, "request.body_shape", "body must be { decision: 'approve' | 'deny', reason? }");
459
662
  return;
460
663
  }
664
+ // The operator decides ACROSS tenants by design (the F4 operator queue), so we bind the decide CAS to the
665
+ // row's OWN scope (read scope-agnostically first) rather than the operator's principal — the scope<=>?
666
+ // clause then becomes an exact, consistent guard instead of a cross-tenant bypass. Unknown id ⇒ 409.
461
667
  const target = await store.getById(id);
462
668
  if (!target) {
463
669
  sendError(res, 409, "conflict.approval_settled", "approval already decided or not found");
@@ -476,10 +682,32 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
476
682
  }
477
683
  miss.fell = true;
478
684
  }
685
+ /** Server-side poll cadence for the native-push approvals stream. The CLIENT subscribes ONCE and never polls;
686
+ * the server diffs at this cadence (TiDB has no LISTEN/NOTIFY) and pushes deltas. 3s < the ~10s client poll it
687
+ * replaces, so the operator sees a new suspend faster AND the portal loses its polling loop. (Each SSE
688
+ * connection runs its OWN poll loop — fine for a handful of operators; a shared fan-out poll is a future
689
+ * optimization if the operator count grows.) */
479
690
  const APPROVALS_STREAM_POLL_MS = 3000;
691
+ /**
692
+ * GET /v1/approvals/stream (design/80 native push): SSE — pushes pending-approval deltas so the portal
693
+ * SUBSCRIBES ONCE instead of polling GET /v1/approvals every ~10s (better UX: near-real-time + no client poll
694
+ * loop). Polls listPending(scope) and diffs against the prior snapshot: `pending` for a new suspend, `resolved`
695
+ * for a decided/expired/gone one. Cross-replica BY CONSTRUCTION — the poll reads the SHARED checkpoint table,
696
+ * so a suspend on ANY replica is seen by an operator streaming on a DIFFERENT replica. A transient DB blip
697
+ * heartbeats + retries (never kills the stream); a 15-min cap + req-close end it (parity with streamTaskTrace).
698
+ * Poll-granularity caveat: a pending that resolves AND re-suspends on the same (session,toolCallId) within one
699
+ * interval shows no delta — acceptable (the live snapshot is always eventually correct; no decision is missed).
700
+ */
480
701
  export async function streamApprovals(req, res, cs, scope, pollMs = APPROVALS_STREAM_POLL_MS) {
702
+ // JSON-encode the (sessionId, toolCallId) pair so distinct pendings can NEVER collide into one Map key,
703
+ // regardless of what a caller-supplied sessionId contains (any single-char delimiter — space OR even NUL — is
704
+ // injectable by an adversarial id, masking/wrong-removing an approval card; adversarial finding). JSON escaping
705
+ // makes every distinct (sessionId, toolCallId) pair a distinct key.
481
706
  const keyOf = (p) => JSON.stringify([p.sessionId, p.toolCallId ?? null]);
482
707
  sseHeaders(res);
708
+ // Each data payload ALSO carries a `type` field mirroring the SSE event name — so a client can dispatch on
709
+ // `data.type` without relying on the SSE `event:` line (some proxies strip event names; `onmessage` ignores
710
+ // named events). The SSE event name stays for native EventSource addEventListener.
483
711
  res.write(`event: meta\ndata: ${JSON.stringify({ type: "meta", version: 1, mode: "approvals-delta", pollMs: APPROVALS_STREAM_POLL_MS })}\n\n`);
484
712
  let closed = false;
485
713
  req.on("close", () => { closed = true; });
@@ -494,6 +722,7 @@ export async function streamApprovals(req, res, cs, scope, pollMs = APPROVALS_ST
494
722
  pending = await cs.listPending(scope);
495
723
  }
496
724
  catch {
725
+ // a transient TiDB blip must NOT kill the subscription — heartbeat + retry next tick (fail-soft)
497
726
  if (!res.writableEnded)
498
727
  res.write(`event: heartbeat\ndata: ${JSON.stringify({ type: "heartbeat" })}\n\n`);
499
728
  await sleep(pollMs);
@@ -501,6 +730,7 @@ export async function streamApprovals(req, res, cs, scope, pollMs = APPROVALS_ST
501
730
  }
502
731
  const cur = new Map(pending.map((p) => [keyOf(p), p]));
503
732
  if (first) {
733
+ // initial snapshot so a freshly-subscribed portal renders the current queue immediately (no first-poll gap)
504
734
  for (const p of pending)
505
735
  res.write(`event: pending\ndata: ${JSON.stringify({ type: "pending", ...p })}\n\n`);
506
736
  res.write(`event: synced\ndata: ${JSON.stringify({ type: "synced", count: pending.length })}\n\n`);
@@ -509,10 +739,10 @@ export async function streamApprovals(req, res, cs, scope, pollMs = APPROVALS_ST
509
739
  else {
510
740
  for (const [k, p] of cur)
511
741
  if (!prev.has(k))
512
- res.write(`event: pending\ndata: ${JSON.stringify({ type: "pending", ...p })}\n\n`);
742
+ res.write(`event: pending\ndata: ${JSON.stringify({ type: "pending", ...p })}\n\n`); // a new suspend
513
743
  for (const [k, p] of prev)
514
744
  if (!cur.has(k))
515
- res.write(`event: resolved\ndata: ${JSON.stringify({ type: "resolved", sessionId: p.sessionId, toolCallId: p.toolCallId })}\n\n`);
745
+ res.write(`event: resolved\ndata: ${JSON.stringify({ type: "resolved", sessionId: p.sessionId, toolCallId: p.toolCallId })}\n\n`); // decided/expired/gone
516
746
  }
517
747
  prev = cur;
518
748
  if (Date.now() - start > MAX_MS) {