@sema-agent/server 1.323.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 +58 -2
  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,13 +1,30 @@
1
+ /**
2
+ * design/158 A9 域模块 —— sandbox-image-pool 控制面(IMAGE-API-DESIGN.md)。
3
+ *
4
+ * 路由:P2 bake 作业面 `/v1/images/bakes*`(提交/列表/详情/事件 SSE/取消/claim/ingest)与 P1 镜像索引
5
+ * `/v1/images*`(digests/profile/select)。**bakes 段必须排在 images 段之前**——P1 的单段 profile 正则会把
6
+ * `bakes` 当成 profile 吞掉(注释原文见路由体内),两段的先后由本文件内部顺序保住。
7
+ *
8
+ * 单域独占状态:`bakeSubmitLimiter`(per-principal 提交限速),按 server 实例存活于 `ctx.local.images`。
9
+ * 本域另收编了原先散在 `server.ts` 模块级的镜像面私产:路由正则、bake 视图投影、claim 应答、ingest 行摄取、
10
+ * bake SSE 包装、以及两个导出的状态/错误码映射(`server.ts` 保留 re-export,导出面不变)。
11
+ */
1
12
  import type { IncomingMessage, ServerResponse } from "node:http";
2
13
  import { RateLimiter } from "../../observability/rate-limit.js";
3
14
  import type { BakeState, BakeErrorCode } from "../../plugins/store-contracts.js";
4
15
  import type { FlatServiceDeps } from "../server.js";
5
16
  import type { RouteCtx } from "../route-ctx.js";
17
+ /** 单域独占状态(lens1 §C2 的 12 条之一)。每个 server 实例一份——模块级会把限速状态跨实例串味
18
+ * (一个进程里建几十个 server 的测试会互相污染)。 */
6
19
  export interface ImagesLocal {
7
20
  bakeSubmitLimiter: RateLimiter;
8
21
  }
9
22
  export declare function createImagesLocal(deps: FlatServiceDeps): ImagesLocal;
23
+ /** build.sh's 8-value `state` → the COARSE 4-value lifecycle the SSE reader polls for terminality (§P2.3). Every
24
+ * NON-terminal state maps to `running` (a PUSHING/VERIFYING state must NOT truncate the stream before `done`). */
10
25
  export declare function coarseStatusForState(state: BakeState): "running" | "done" | "failed";
26
+ /** build.sh's structured exit code → the center band-composer UI's terminal `errorCode` (§P2.14 #2). null for an
27
+ * uncategorized failure (then `error` free-text + `exitCode` drive the UI). 10=dup-tag, 124=timeout, 507=disk. */
11
28
  export declare function errorCodeForExit(exitCode: number | null | undefined, cancelled: boolean): BakeErrorCode;
12
29
  export declare const IMAGE_DIGEST_RE: RegExp;
13
30
  export declare const IMAGE_PROFILE_RE: RegExp;
@@ -8,8 +8,14 @@ import { sendJson, sendError } from "../send.js";
8
8
  import { headerStr, safeEqual, gatedPrincipal, explicitOperatorOk, explicitOperator } from "../principal-gate.js";
9
9
  import { scopedIdempotencyKey } from "../idempotency.js";
10
10
  export function createImagesLocal(deps) {
11
+ // Per-principal bake-submission rate guard (IMAGE-API-DESIGN.md §P2.4c — fills the register DoS gap the images
12
+ // block never guarded). Separate from the global rateLimiter (which may be off): an unbounded POST /bakes flood
13
+ // is real resource-exhaustion churn even though the single-flight lease caps concurrent BUILDS at 1. Off when
14
+ // submitRateMax<=0. Swept lazily by `check`'s window roll-over (no timer needed for a small operator set).
11
15
  return { bakeSubmitLimiter: new RateLimiter(deps.config.imageBakes?.submitRateMax ?? 0, (deps.config.imageBakes?.submitRateWindowSec ?? 60) * 1000) };
12
16
  }
17
+ /** build.sh's 8-value `state` → the COARSE 4-value lifecycle the SSE reader polls for terminality (§P2.3). Every
18
+ * NON-terminal state maps to `running` (a PUSHING/VERIFYING state must NOT truncate the stream before `done`). */
13
19
  export function coarseStatusForState(state) {
14
20
  if (state === "COMPLETE")
15
21
  return "done";
@@ -17,26 +23,40 @@ export function coarseStatusForState(state) {
17
23
  return "failed";
18
24
  return "running";
19
25
  }
26
+ /** build.sh's structured exit code → the center band-composer UI's terminal `errorCode` (§P2.14 #2). null for an
27
+ * uncategorized failure (then `error` free-text + `exitCode` drive the UI). 10=dup-tag, 124=timeout, 507=disk. */
20
28
  export function errorCodeForExit(exitCode, cancelled) {
21
29
  if (cancelled)
22
30
  return "cancelled";
23
31
  switch (exitCode) {
24
32
  case 9:
25
- return "band-conflict";
33
+ return "band-conflict"; // build.sh's generate-time inter-band collision (gen-dockerfile exit 6/7/8 → 9).
26
34
  case 10:
27
35
  return "duplicate";
28
36
  case 124:
29
37
  return "timeout";
30
38
  case 507:
31
- return "disk";
39
+ return "disk"; // belt: the runner's pre-flight disk guard (runner.ts) exits 507 — categorize it even if the
40
+ // structured line.errorCode were ever absent (M1: line.errorCode is the primary source).
32
41
  default:
33
42
  return null;
34
43
  }
35
44
  }
45
+ /** The closed set of structured terminal `errorCode`s a runner/build.sh frame may carry (§P2.14 #2). A
46
+ * runner-supplied code is a UI hint only, but it CANNOT reconstruct from the exit code alone (e.g. the disk
47
+ * guard's 507, or a future band-conflict 6/7/8), so ingestBakeLine PREFERS a validated `line.errorCode` over
48
+ * the exit-code recompute. Set-membership keeps it inside the BakeErrorCode union (never trust a free-text). */
36
49
  const RUNNER_ERROR_CODES = new Set(["band-conflict", "duplicate", "disk", "timeout", "cancelled"]);
50
+ /** The index `profile` under which the FROM base (`sandbox-base`) is published — the ONLY profile a bake's
51
+ * `--base-ref` may resolve to (a published code-* profile digest is a built layer, never a valid FROM base).
52
+ * Both the caller-supplied-baseRef allow-list AND the default-base fallback resolve against this profile. */
37
53
  const SANDBOX_BASE_PROFILE = "sandbox-base";
54
+ // sandbox-image-pool query routes (IMAGE-API-DESIGN.md §2). `/digests/:digest` is checked BEFORE `/:profile`
55
+ // (the single-segment profile RE would otherwise swallow "digests"); both are GET-only + visibility-scoped.
38
56
  export const IMAGE_DIGEST_RE = /^\/v1\/images\/digests\/([^/]+)$/;
39
57
  export const IMAGE_PROFILE_RE = /^\/v1\/images\/([^/]+)$/;
58
+ // sandbox-image-pool BAKE routes (IMAGE-API-DESIGN.md §P2.1). No trailing-slash variant (`/v1/images/bakes/` →
59
+ // 404, matching the live `/v1/images/` behavior). The `:bakeId` group + sub-action are both-or-neither.
40
60
  export const BAKE_ID_RE = /^\/v1\/images\/bakes\/([^/]+)$/;
41
61
  export const BAKE_EVENTS_RE = /^\/v1\/images\/bakes\/([^/]+)\/events$/;
42
62
  export const BAKE_CANCEL_RE = /^\/v1\/images\/bakes\/([^/]+)\/cancel$/;
@@ -47,24 +67,40 @@ export async function handleImages(req, res, url, ctx) {
47
67
  await handleImagesBody(req, res, url, ctx, miss);
48
68
  return !miss.fell;
49
69
  }
70
+ /** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。裸 `return;`
71
+ * 的语义原样 =「本域已应答」;走到函数尾才是「没匹配上」,置 `miss.fell` 交回装配器继续下一个域。
72
+ * 跨域的东西(deps / 助手 / 单域状态 / 每请求量)一律在首行解构出来,于是下面每一行都逐字不变。 */
50
73
  async function handleImagesBody(req, res, url, ctx, miss) {
51
74
  const { deps } = ctx;
52
75
  const { readJson } = ctx.helpers;
53
76
  const { source } = ctx.req;
54
77
  const { bakeSubmitLimiter } = ctx.local.images;
78
+ // ── sandbox-image-pool BAKE control plane (IMAGE-API-DESIGN.md §P2). MUST precede the P1 images block: the P1
79
+ // profile RE `/^\/v1\/images\/([^/]+)$/` would otherwise swallow `/v1/images/bakes` as profile="bakes" (404),
80
+ // and a two-segment `/v1/images/bakes/:id` would fall through P1 with NO response (hung socket). image-api is
81
+ // the UNPRIVILEGED controller: it never bakes — it owns the durable job row/event log/lease, the operator gate,
82
+ // and the input whitelist; the privileged build runs on the bake-runner (build-host) over the internal claim/ingest.
55
83
  if (deps.imageBakes && url.startsWith("/v1/images/bakes")) {
56
84
  const bakes = deps.imageBakes;
57
- const idx = deps.imageIndex;
58
- const principal = gatedPrincipal(req, deps.config);
85
+ const idx = deps.imageIndex; // the auto-register target (wired together with imageBakes in main.ts)
86
+ // Identity resolved EXACTLY as P1 (direct-door JWT-without-cnf.bnd, else principalFrom).
87
+ const principal = gatedPrincipal(req, deps.config); // direct-door safe — identity-only proof; single source of truth (see gatedPrincipal)
59
88
  const operators = deps.config.operatorPrincipals;
60
89
  const explicitOperator = explicitOperatorOk(principal, operators);
61
90
  const cfg = deps.config.imageBakes;
91
+ // 🔴 H1: the bake-runner identity is DERIVED from its bearer CREDENTIAL (the token-derived `source`,
92
+ // resolved above from SERVICE_AUTH_TOKENS), NOT a self-asserted x-agent-principal/JWT the runner never
93
+ // sends. `source` is independent of the directDoorActive principal ternary, so this works identically in
94
+ // direct-door AND plain mode. The config boot invariant guarantees a runner-credential→runnerPrincipal
95
+ // mapping exists when bakes are enabled.
62
96
  const isRunner = source !== undefined && source !== null && source === cfg.runnerPrincipal;
97
+ // POST /v1/images/bakes — operator-only submit. EXPLICIT operator gate (NOT the bare isOperator; §P2.4a).
63
98
  if (req.method === "POST" && url === "/v1/images/bakes") {
64
99
  if (!explicitOperator) {
65
100
  sendError(res, 403, "auth.operator_only", "operator only");
66
101
  return;
67
102
  }
103
+ // Per-principal submission rate guard (§P2.4c) — caps queue-churn even though the lease caps live builds at 1.
68
104
  const rl = bakeSubmitLimiter.check(`bake:${principal}`);
69
105
  if (!rl.allowed) {
70
106
  res.setHeader("retry-after", String(rl.retryAfterSec));
@@ -72,11 +108,22 @@ async function handleImagesBody(req, res, url, ctx, miss) {
72
108
  return;
73
109
  }
74
110
  const body = (await readJson(req));
111
+ // Input WHITELIST — set-membership BEFORE any argv is built (§P2.4d). Off-list → 400/501, never coerced.
75
112
  const v = validateBake(body);
113
+ // B7:errorCode 恒在——band 冲突保留既有 `band-conflict` 机器码(1.x 契约,勿改);其余 400=字段白名单
114
+ // 拒收、501=该形 bake 尚未开面(custom/tenant P4)。`error` 文案与键序逐字不变。
76
115
  if (!v.ok) {
77
116
  sendError(res, v.status, v.conflictBands ? "band-conflict" : v.status === 501 ? "capability.unavailable" : "request.field_invalid", v.error, { ...(v.conflictBands ? { conflictBands: v.conflictBands } : {}) });
78
117
  return;
79
118
  }
119
+ // baseRef allow-list (§P2.4c): a caller CANNOT pass an arbitrary FROM. A present baseRef must be a KNOWN
120
+ // PUBLISHED `sandbox-base` digest the index has seen (a syntactically-valid attacker digest is still
121
+ // arbitrary; a published code-* PROFILE digest is also not a valid FROM base — the profile must be
122
+ // sandbox-base). Absent ⇒ the pinned default base = `SANDBOX_BASE_REF` OR the index's latest published
123
+ // sandbox-base (design §P2.4c "SANDBOX_BASE_REF / the index's latest published base"). The shape was
124
+ // already checked in validateBake. The digest the server resolves is the SAME for push:true and
125
+ // push:false — a node-local (no-push) build's host docker resolves a sandbox-base@sha256 FROM against
126
+ // the locally-present base image by its config digest (verified live), so one digest serves both paths.
80
127
  let effectiveBaseRef;
81
128
  if (v.baseRef) {
82
129
  const baseEntry = idx ? await idx.getByDigest(v.baseRef) : null;
@@ -87,15 +134,25 @@ async function handleImagesBody(req, res, url, ctx, miss) {
87
134
  effectiveBaseRef = v.baseRef;
88
135
  }
89
136
  else {
137
+ // SANDBOX_BASE_REF pin first; else the index's latest published sandbox-base (operator viewer — the base
138
+ // pool is server-internal, never tenant-scoped). The server still WHOLLY chooses the base (the caller
139
+ // never influences it), so the anti-RCE property holds.
90
140
  effectiveBaseRef = cfg.defaultBaseRef ?? (idx ? (await idx.latestPublished(SANDBOX_BASE_PROFILE, { operator: true }))?.digest ?? null : null);
91
141
  }
142
+ // A real (non-dryRun) build MUST FROM a concrete base — build.sh dies (exit 3) on a missing --base-ref.
143
+ // Fail CLOSED here with an honest config error rather than queue an argv that can never build (a dryRun is
144
+ // resolve-only and legitimately previews with build.sh's floating base, so it's exempt).
92
145
  if (!v.dryRun && !effectiveBaseRef) {
93
146
  sendError(res, 503, "state.no_sandbox_base", "no sandbox-base available to build from (set SANDBOX_BASE_REF or publish a sandbox-base image)");
94
147
  return;
95
148
  }
96
149
  const argv = buildBakeArgv(v, { baseRef: effectiveBaseRef, cacheBase: cfg.cacheBase });
97
150
  const rawIdem = headerStr(req.headers["idempotency-key"]) ?? (typeof body.idempotencyKey === "string" ? body.idempotencyKey : undefined);
151
+ // Durable submit-idempotency: scope the key to the submitting principal (BL-3 parity) so a replay is the
152
+ // SAME caller only; the store's `idem_key` UNIQUE is the durable dedupe.
98
153
  const idemKey = rawIdem ? scopedIdempotencyKey(rawIdem, source, principal) ?? null : null;
154
+ // §P2.1/§P2.7 single-flight at SUBMIT (concurrency 1; no auto-queue). A `dryRun` is resolve-only — it never
155
+ // takes the lease, so it bypasses the busy guard entirely (parallel-safe) and uses the plain createBake.
99
156
  const createInput = {
100
157
  profile: v.profile,
101
158
  bands: v.bands,
@@ -108,11 +165,18 @@ async function handleImagesBody(req, res, url, ctx, miss) {
108
165
  requestedBy: principal ?? null,
109
166
  };
110
167
  if (!v.dryRun) {
168
+ // (a) an IDENTICAL-argv in-flight bake ⇒ attach (idempotent re-submit, 202) — no second build.
111
169
  const sameArgv = await bakes.findActiveByArgv(argv);
112
170
  if (sameArgv) {
113
171
  sendJson(res, 202, { bakeId: sameArgv.bakeId, eventsUrl: `/v1/images/bakes/${sameArgv.bakeId}/events`, status: sameArgv.status, state: sameArgv.state });
114
172
  return;
115
173
  }
174
+ // (b) M4: ATOMIC admission — the busy-check and the create are ONE DB statement (`INSERT … SELECT …
175
+ // WHERE NOT EXISTS (a non-dryRun in-flight bake)`), so two concurrent keyless POSTs can never both queue
176
+ // (the old findActiveAny()→createBake() check-then-act interleaved). On `created:false` a DIFFERENT bake
177
+ // holds (or will hold) the single-flight lease ⇒ re-read the active bake. Do NOT silently queue a 45-min
178
+ // build; surface the active bake + its events URL so the UI attaches to the live one (or attach if a
179
+ // concurrent identical-argv submit won the race in the gap).
116
180
  const r = await bakes.createBakeIfIdle(createInput);
117
181
  if (!r.created) {
118
182
  const sameNow = await bakes.findActiveByArgv(argv);
@@ -131,6 +195,7 @@ async function handleImagesBody(req, res, url, ctx, miss) {
131
195
  sendJson(res, 202, { bakeId: bake.bakeId, eventsUrl: `/v1/images/bakes/${bake.bakeId}/events`, status: bake.status, state: bake.state });
132
196
  return;
133
197
  }
198
+ // GET /v1/images/bakes/:bakeId/events — resumable SSE (the live "baking…" view), run-trace-SSE-shaped (§P2.2).
134
199
  const evMatch = req.method === "GET" ? BAKE_EVENTS_RE.exec(url) : null;
135
200
  if (evMatch) {
136
201
  if (!explicitOperator) {
@@ -146,6 +211,7 @@ async function handleImagesBody(req, res, url, ctx, miss) {
146
211
  await streamBakeEvents(req, res, bakes, bakeId, cfg.staleMs);
147
212
  return;
148
213
  }
214
+ // POST /v1/images/bakes/:bakeId/cancel — cooperative cancel (durable flag; the runner kills the pgid, §P2.11).
149
215
  const cancelMatch = req.method === "POST" ? BAKE_CANCEL_RE.exec(url) : null;
150
216
  if (cancelMatch) {
151
217
  if (!explicitOperator) {
@@ -158,31 +224,39 @@ async function handleImagesBody(req, res, url, ctx, miss) {
158
224
  sendError(res, 404, "not_found.bake", "bake not found");
159
225
  return;
160
226
  }
161
- const flagged = await bakes.requestCancel(bakeId);
227
+ const flagged = await bakes.requestCancel(bakeId); // only flags a non-terminal row
162
228
  sendJson(res, 202, { bakeId, cancelRequested: true, note: flagged ? "cancel requested — the runner kills the build at the next heartbeat" : "bake already terminal — cancel is a no-op" });
163
229
  return;
164
230
  }
231
+ // POST /v1/images/bakes/claim — RUNNER-ONLY: long-poll claim (§P2.12). Find the oldest queued bake and claim
232
+ // it in one call (the discovery + the single-flight CAS). Checked BEFORE the per-bake `…/:id/claim` so the
233
+ // bare `claim` is not swallowed as `:bakeId="claim"`. 204 when the queue is empty (the runner idle-polls).
165
234
  if (req.method === "POST" && url === "/v1/images/bakes/claim") {
166
235
  if (!isRunner) {
167
236
  sendError(res, 403, "auth.bake_runner_only", "bake-runner only");
168
237
  return;
169
238
  }
170
239
  const body = (await readJson(req));
240
+ // H1: the runner is credential-authed (no x-agent-principal), so `principal` may be undefined — fall back
241
+ // to the credential-derived `source` (== runnerPrincipal, isRunner guaranteed it) as the runner identity.
171
242
  const runnerId = typeof body.runnerId === "string" && body.runnerId.length > 0 ? body.runnerId : (principal ?? source ?? cfg.runnerPrincipal);
243
+ // Bounded find-then-CAS: another runner racing us may win the id we just read, so retry the next queued id
244
+ // a few times before giving up (the queue is normally short + replicas=1, so 1 pass is the common case).
172
245
  let claimed = null;
173
246
  for (let attempt = 0; attempt < 4 && !claimed; attempt++) {
174
247
  const nextId = await bakes.findNextQueuedId();
175
248
  if (!nextId)
176
- break;
249
+ break; // queue empty
177
250
  claimed = await bakes.claimBake(nextId, runnerId, cfg.leaseMs);
178
251
  }
179
252
  if (!claimed) {
180
253
  res.writeHead(204).end();
181
254
  return;
182
- }
255
+ } // nothing claimable right now (the runner idle-polls)
183
256
  sendJson(res, 200, claimResponse(claimed));
184
257
  return;
185
258
  }
259
+ // POST /v1/images/bakes/:bakeId/claim — RUNNER-ONLY: single-flight lease of THIS queued bake (§P2.7).
186
260
  const claimMatch = req.method === "POST" ? BAKE_CLAIM_RE.exec(url) : null;
187
261
  if (claimMatch) {
188
262
  if (!isRunner) {
@@ -194,12 +268,16 @@ async function handleImagesBody(req, res, url, ctx, miss) {
194
268
  const runnerId = typeof body.runnerId === "string" && body.runnerId.length > 0 ? body.runnerId : (principal ?? source ?? cfg.runnerPrincipal);
195
269
  const claimed = await bakes.claimBake(bakeId, runnerId, cfg.leaseMs);
196
270
  if (!claimed) {
271
+ // Lost the single-flight (already claimed, or the pool lease is held by a live build) → 409.
197
272
  sendError(res, 409, "conflict.bake_not_claimable", "bake not claimable (already claimed or the build lease is held)");
198
273
  return;
199
274
  }
275
+ // Return the VETTED argv + the per-bake ingest secret (minted at claim) the runner echoes on ingest.
200
276
  sendJson(res, 200, claimResponse(claimed));
201
277
  return;
202
278
  }
279
+ // POST /v1/images/bakes/:bakeId/ingest — RUNNER-ONLY: append one build.sh line + heartbeat the lease.
280
+ // Maps build.sh `{event}` → an SSE kind + the coarse status; on a COMPLETE `done` triggers auto-register.
203
281
  const ingestMatch = req.method === "POST" ? BAKE_INGEST_RE.exec(url) : null;
204
282
  if (ingestMatch) {
205
283
  if (!isRunner) {
@@ -212,12 +290,16 @@ async function handleImagesBody(req, res, url, ctx, miss) {
212
290
  sendError(res, 404, "not_found.bake", "bake not found");
213
291
  return;
214
292
  }
293
+ // Per-bake ingest secret (minted at claim) — so a stale/rogue runner can't inject frames into another bake.
215
294
  const presented = headerStr(req.headers["x-bake-ingest-secret"]);
216
295
  if (!bake.ingestSecret || !presented || !safeEqual(presented, bake.ingestSecret)) {
217
296
  sendError(res, 403, "auth.ingest_secret_invalid", "invalid ingest secret");
218
297
  return;
219
298
  }
220
299
  const line = (await readJson(req));
300
+ // A `heartbeat` frame (§P2.7/§P2.11): advance the lease, append NO event, and echo the durable cancel flag +
301
+ // lease validity so the runner kills the build.sh process group at the next tick. A lost lease (the reaper
302
+ // stole it as stale, or a terminal flip released it) → 409 so the runner aborts (it no longer owns the build).
221
303
  if (line.event === "heartbeat") {
222
304
  const fresh = await bakes.getBake(bakeId);
223
305
  if (!fresh || fresh.status !== "running") {
@@ -232,11 +314,17 @@ async function handleImagesBody(req, res, url, ctx, miss) {
232
314
  sendJson(res, 200, { accepted: true, leaseValid: true, cancelRequested: fresh.cancelRequested });
233
315
  return;
234
316
  }
317
+ // 🔴 H3 (zombie/reaped runner) — PRIMARY guard: a terminal/reaped bake accepts NO frames. Re-read the row
318
+ // (the `bake` fetched above may be stale) and reject 409 for ALL non-heartbeat frames when it is no longer
319
+ // running, mirroring the heartbeat branch. Otherwise a zombie/duplicate runner that still holds a valid
320
+ // ingest_secret could inject `building` index rows + out-of-band SSE frames into a dead bake's stream.
235
321
  const fresh = await bakes.getBake(bakeId);
236
322
  if (!fresh || fresh.status !== "running") {
237
323
  sendJson(res, 409, { accepted: false, leaseValid: false, cancelRequested: fresh?.cancelRequested ?? false, errorCode: "conflict.bake_not_running" });
238
324
  return;
239
325
  }
326
+ // A real build.sh frame: also advance the lease (the runner heartbeats via its frame cadence too) and echo
327
+ // the cancel flag so a cancel observed between heartbeats still reaches the runner promptly.
240
328
  if (fresh.runnerId) {
241
329
  await bakes.heartbeatLease(bakeId, fresh.runnerId, cfg.leaseMs).catch(() => undefined);
242
330
  }
@@ -245,6 +333,8 @@ async function handleImagesBody(req, res, url, ctx, miss) {
245
333
  sendJson(res, result.status, { ...result.body, cancelRequested: after?.cancelRequested ?? false, leaseValid: true });
246
334
  return;
247
335
  }
336
+ // GET /v1/images/bakes/:bakeId — poll fallback, `{bake:}` envelope, null-not-omitted (§P2.1). Checked LAST so
337
+ // the `/events|/cancel|/claim|/ingest` sub-actions (two-segment) win first.
248
338
  const idMatch = req.method === "GET" ? BAKE_ID_RE.exec(url) : null;
249
339
  if (idMatch) {
250
340
  if (!explicitOperator) {
@@ -260,15 +350,25 @@ async function handleImagesBody(req, res, url, ctx, miss) {
260
350
  sendJson(res, 200, { bake: bakeView(bake) });
261
351
  return;
262
352
  }
353
+ // No trailing-slash variant, no other bake route → 404 (parity with `/v1/images/` live behavior).
263
354
  sendError(res, 404, "not_found.route", "not found");
264
355
  return;
265
356
  }
357
+ // ── sandbox-image-pool control API (IMAGE-API-DESIGN.md §2, pool design §8) — P1: query + select. The image
358
+ // INDEX is queried/selected here (bake/publish = P2/P3, later). Visibility is viewer-scoped (Codex A1): a
359
+ // tenant principal sees `public` + its OWN tenant images; an operator sees all. Identity is resolved the same
360
+ // way as the approval reads (direct-door JWT-without-cnf.bnd, else principalFrom); an absent principal still
361
+ // sees the public catalog (anonymous public-pull parity — pool = docker.io/claybobby; SWR deprecated) but no tenant rows.
266
362
  if (deps.imageIndex && url.startsWith("/v1/images")) {
267
363
  const idx = deps.imageIndex;
268
- const principal = gatedPrincipal(req, deps.config);
364
+ const principal = gatedPrincipal(req, deps.config); // direct-door safe — identity-only proof; single source of truth (see gatedPrincipal)
365
+ // explicitOperator (NOT isOperator): for image VISIBILITY, an empty OPERATOR_PRINCIPALS must NOT make every
366
+ // caller an operator (that would leak tenant-scoped images). Adversarial-review HIGH — same fix as the P0.5
367
+ // per-task resolve in main.ts. (Approval operator gates below keep isOperator's legacy empty=all boundary.)
269
368
  const operator = explicitOperator(principal, deps.config.operatorPrincipals);
270
369
  const viewer = { operator, tenantId: principal ?? null };
271
370
  const q = new URL(req.url ?? "", "http://x").searchParams;
371
+ // GET /v1/images — the paginated catalog. ?profile=&status=&capability=&latest=true&limit=&cursor=
272
372
  if (req.method === "GET" && url === "/v1/images") {
273
373
  const limRaw = q.get("limit");
274
374
  const { entries, nextCursor } = await idx.list({
@@ -283,6 +383,9 @@ async function handleImagesBody(req, res, url, ctx, miss) {
283
383
  sendJson(res, 200, { images: entries, nextCursor });
284
384
  return;
285
385
  }
386
+ // POST /v1/images/select — the §7 per-sandbox resolution: profile → newest published visible digest +
387
+ // podContract, with capability admission (Codex B3). Pure resolution (no build); the caller threads the
388
+ // returned `ref` (repo@digest) into the kata adapter's per-sandbox image (today a worker-global env).
286
389
  if (req.method === "POST" && url === "/v1/images/select") {
287
390
  const body = (await readJson(req));
288
391
  const profile = typeof body.profile === "string" ? body.profile : "";
@@ -315,7 +418,12 @@ async function handleImagesBody(req, res, url, ctx, miss) {
315
418
  });
316
419
  return;
317
420
  }
421
+ // POST /v1/images/register — operator-only ingest (P1: seed from the live bake manifests; P3 atomic-publish
422
+ // drives it for real). Minimal validation here; the publish pipeline supplies a full manifest projection.
318
423
  if (req.method === "POST" && url === "/v1/images/register") {
424
+ // EXPLICIT operator gate (§P2.4a MUST-FIX, the P1 follow-up): NOT the bare `operator`/isOperator, whose
425
+ // empty-OPERATOR_PRINCIPALS "true-for-all" makes this index-mutating door world-writable. (The block-level
426
+ // `operator` stays for read visibility; the MUTATION must require a concrete configured operator.)
319
427
  if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
320
428
  sendError(res, 403, "auth.operator_only", "operator only");
321
429
  return;
@@ -325,6 +433,11 @@ async function handleImagesBody(req, res, url, ctx, miss) {
325
433
  sendError(res, 400, "request.field_invalid", "profile, repo, digest are required");
326
434
  return;
327
435
  }
436
+ // 🔴 STATUS-MONOTONIC register (adversarial-review HIGH finding): the unconditional `upsert` let an
437
+ // operator POST status:'building' for an already-`published` digest and SILENTLY DE-PUBLISH a live image
438
+ // (latestPublished then 404s → sandbox-create fails — the P2.10 bug class, LIVE on this door). registerBuilding
439
+ // never downgrades a published row (building→published still works; published→building is a no-op on the
440
+ // lifecycle). A deliberate take-down is the explicit deprecate path (P3), never a side effect of register.
328
441
  const id = await idx.registerBuilding({
329
442
  profile: b.profile,
330
443
  bands: Array.isArray(b.bands) ? b.bands : [],
@@ -348,6 +461,7 @@ async function handleImagesBody(req, res, url, ctx, miss) {
348
461
  sendJson(res, 200, { id });
349
462
  return;
350
463
  }
464
+ // GET /v1/images/digests/:digest — one entry by immutable digest (what a running sandbox was built from).
351
465
  const digMatch = req.method === "GET" ? IMAGE_DIGEST_RE.exec(url) : null;
352
466
  if (digMatch) {
353
467
  const entry = await idx.getByDigest(decodeURIComponent(digMatch[1]));
@@ -358,6 +472,7 @@ async function handleImagesBody(req, res, url, ctx, miss) {
358
472
  sendJson(res, 200, { image: entry });
359
473
  return;
360
474
  }
475
+ // GET /v1/images/:profile — the newest published visible entry for a profile (latest tag + versions).
361
476
  const profMatch = req.method === "GET" ? IMAGE_PROFILE_RE.exec(url) : null;
362
477
  if (profMatch) {
363
478
  const entry = await idx.latestPublished(decodeURIComponent(profMatch[1]), viewer);
@@ -371,6 +486,9 @@ async function handleImagesBody(req, res, url, ctx, miss) {
371
486
  }
372
487
  miss.fell = true;
373
488
  }
489
+ /** The image_bake SSE (P2.2/P2.8) — the SAME pump, with the run-trace-SSE-shaped bake frame
490
+ * (`data: {kind, seq, ts, ...kindFields}`) + a synthetic `done{failed}` stale frame so a reader settles
491
+ * (never hangs) when the runner is lost before the reaper's terminal event lands. */
374
492
  async function streamBakeEvents(req, res, bakes, bakeId, staleMs) {
375
493
  await streamSseLog(req, res, {
376
494
  statusOf: async (id) => {
@@ -379,6 +497,8 @@ async function streamBakeEvents(req, res, bakes, bakeId, staleMs) {
379
497
  },
380
498
  getEvents: (id, after) => bakes.getEvents(id, after),
381
499
  retainedFrom: (id) => bakes.retainedFrom(id),
500
+ // The bake frame carries kind+seq+ts INSIDE data (center locks its relay to this), distinct from the
501
+ // task_run `{type, ...data}` shape. `level` rides along so an opt-in `raw` log frame is distinguishable.
382
502
  formatEvent: (ev) => ({
383
503
  id: ev.seq,
384
504
  event: ev.kind,
@@ -387,6 +507,9 @@ async function streamBakeEvents(req, res, bakes, bakeId, staleMs) {
387
507
  staleFrame: () => ({ event: "done", data: { kind: "done", status: "FAILED", error: "bake-runner lost (no heartbeat)", digest: null, indexId: null, repo: null, ref: null } }),
388
508
  }, bakeId, staleMs);
389
509
  }
510
+ /** The operator poll view of a bake (§P2.1 `{bake:}` envelope, null-not-omitted). Projects the durable row MINUS
511
+ * the runner-internal secret/lease fields (`ingestSecret`/`runnerId`/`leaseUntil`/`cancelRequested`/`argv`) — an
512
+ * ops surface, but the per-bake ingest secret is a credential and the argv is build-host detail. */
390
513
  function bakeView(b) {
391
514
  return {
392
515
  bakeId: b.bakeId,
@@ -412,6 +535,9 @@ function bakeView(b) {
412
535
  updatedAt: b.updatedAt,
413
536
  };
414
537
  }
538
+ /** The RUNNER claim response (§P2.7/§P2.12): the VETTED argv + the per-bake ingest secret (minted at claim, echoed
539
+ * on every ingest) + the profile/bands the runner needs for its profile-aware hard deadline. This is the ONLY
540
+ * place the ingest secret leaves image-api — to the runner that just won the lease, over the internal claim. */
415
541
  function claimResponse(claimed) {
416
542
  return {
417
543
  bakeId: claimed.bakeId,
@@ -425,76 +551,143 @@ function claimResponse(claimed) {
425
551
  leaseUntil: claimed.leaseUntil,
426
552
  };
427
553
  }
554
+ /** Map ONE build.sh JSON line (runner-forwarded, with `lineOrd`) → the durable bake event log + the coarse status
555
+ * transition, and on a `done:COMPLETE` trigger the status-monotonic auto-register (§P2.3/§P2.10).
556
+ *
557
+ * build.sh emits `resolved | state | step | image | manifest | done` (read, not guessed). The runner forwards each
558
+ * verbatim plus a per-line `lineOrd` (at-least-once dedupe, uq_line). `done`/`manifest` carry a build-host FS path, not
559
+ * the body, so the runner reads the manifest body off the host and forwards it as `manifestBody` on the `done`.
560
+ *
561
+ * The COARSE status (the reader's terminality column) is driven ONLY by `state`/`done` (§P2.3 MUST-FIX): every
562
+ * non-terminal state → `running`; COMPLETE→done, FAILED/CANCELLED→failed. A `state` line that is non-terminal only
563
+ * denormalizes the UI progress bar (never the coarse status). The terminal `done` is appended BEFORE the coarse
564
+ * row flips (event-before-row latch) so the SSE reader always observes it.
565
+ */
428
566
  async function ingestBakeLine(bakes, idx, bake, line, registry) {
429
567
  const bakeId = bake.bakeId;
430
568
  const event = typeof line.event === "string" ? line.event : "";
431
569
  const lineOrd = typeof line.lineOrd === "number" ? line.lineOrd : typeof line.line_ord === "number" ? line.line_ord : null;
570
+ // A non-`done` build.sh line: append it as the matching kind + denormalize `state` for the UI progress bar.
571
+ // (`resolved | state | step | image | manifest | log` — `manifest` is folded; the body rides the `done`.)
432
572
  if (event !== "done") {
433
573
  if (event === "state") {
434
574
  const state = typeof line.state === "string" ? line.state : null;
435
575
  if (state)
436
- await bakes.setState(bakeId, state);
576
+ await bakes.setState(bakeId, state); // denormalize only — NEVER the coarse terminality (§P2.3)
437
577
  const seq = await bakes.appendEvent(bakeId, "state", { state }, { lineOrd });
438
578
  return { status: 200, body: { accepted: true, seq } };
439
579
  }
580
+ // resolved | step | image | log | manifest → store under the build.sh event name as the SSE kind (the runner
581
+ // already shapes the data fields; manifest is kept as a `step`-adjacent frame for the trace, body on `done`).
440
582
  const kind = event === "manifest" ? "manifest" : event;
441
583
  const level = event === "log" && line.level === "raw" ? "raw" : null;
442
584
  const { event: _e, lineOrd: _lo, line_ord: _li, ...data } = line;
585
+ // L3 (defense-in-depth): redactDeep the ingested non-terminal frame data before it's stored/streamed, mirroring
586
+ // streamTaskTrace's tool-arg redaction. build.sh keeps raw cred output off stdout (only resolved.mergedEnv —
587
+ // frozen server recipes — is unredacted today), but this closes the resolved.mergedEnv tail + any future
588
+ // `?logs=1` raw-tail before it's wired. (redactSecrets is non-exhaustive — hardening, not a guarantee.)
443
589
  const seq = await bakes.appendEvent(bakeId, kind, redactDeep(data), { lineOrd, level });
444
590
  return { status: 200, body: { accepted: true, seq } };
445
591
  }
446
- const doneStatus = typeof line.status === "string" ? line.status : "FAILED";
592
+ // ── terminal `done` drive the auto-register + the coarse terminal flip (§P2.10).
593
+ const doneStatus = typeof line.status === "string" ? line.status : "FAILED"; // COMPLETE | FAILED | CANCELLED
447
594
  const exitCode = typeof line.exitCode === "number" ? line.exitCode : doneStatus === "COMPLETE" ? 0 : null;
448
595
  const cancelled = doneStatus === "CANCELLED";
449
596
  const errorText = typeof line.error === "string" ? redactSecrets(line.error) : null;
450
597
  const tag = typeof line.tag === "string" ? line.tag : bake.tag;
451
- let digest = typeof line.digest === "string" && line.digest.length > 0 ? line.digest : null;
598
+ let digest = typeof line.digest === "string" && line.digest.length > 0 ? line.digest : null; // let:R22 身份折叠可置空
452
599
  let indexId = bake.indexId;
453
600
  let repo = bake.repo;
454
601
  let ref = bake.ref;
455
602
  let manifestSha = bake.manifestSha;
603
+ // The done STATUS/error can be DOWNGRADED to FAILED on a register-after-build failure (the partial-publish
604
+ // contract below) — start from build.sh's reported status and mutate iff the register throws.
456
605
  let finalStatus = doneStatus;
457
606
  let finalError = errorText;
458
607
  let finalExit = exitCode;
608
+ // M1: a register-after-build downgrade (COMPLETE→FAILED) must OWN the terminal errorCode — a stale COMPLETE-time
609
+ // runner code (e.g. none) must not mask the partial-publish failure. Tracked here, consulted at the errorCode pick.
459
610
  let registerDowngraded = false;
611
+ // Auto-register (only a COMPLETE with a manifest body + a digest is registerable). EVENT-BEFORE-ROW: we register
612
+ // (idx write) and append the `done` SSE event BEFORE the coarse terminal flip, so a `done` is always observable
613
+ // even if the terminal write retries.
614
+ //
615
+ // PARTIAL-PUBLISH CONTRACT (§P2.2/§P2.10): a register-after-build failure (a TiDB blip AFTER build.sh COMPLETE)
616
+ // is surfaced as `digest` present + `indexId` null + `status:"FAILED"` — a built-but-unregistered partial, NOT a
617
+ // successful COMPLETE and NOT a re-bake trigger (a P3 reconciliation tick re-registers it). So the done status is
618
+ // downgraded COMPLETE→FAILED on the catch, with the digest/repo/ref still carried so the artifact is findable.
619
+ // M3: the `register` step frames are EVENT-LOG writes that must follow the SAME terminality gate as the done
620
+ // event — buffer them here and replay only when THIS call wins the setTerminal CAS (affectedRows>=1). The idx
621
+ // writes (registerBuilding/setIndexId) are status-monotonic and idempotent, so they may run before the CAS.
460
622
  const registerSteps = [];
461
623
  if (doneStatus === "COMPLETE" && digest && idx) {
462
624
  const manifestBody = (line.manifestBody ?? line.manifest);
625
+ // 🔴 H3 (zombie/reaped runner): re-read the bake row HERE (the passed `bake` may be stale) and ONLY register
626
+ // when it is still non-terminal. A reaped/terminal row must never inject a fresh `building` index row.
463
627
  const fresh = await bakes.getBake(bakeId);
464
628
  const nonTerminal = !!fresh && (fresh.status === "queued" || fresh.status === "running");
465
629
  if (manifestBody && typeof manifestBody === "object" && nonTerminal) {
630
+ // 🔴 H3/H5: derive repo/profile from the VETTED control-plane bake row (+ the manifest's profile only as a
631
+ // fallback for a --bands "custom" bake) — NEVER trust the runner-supplied manifestBody for the server-fixed
632
+ // repo identity. H5: prefix the configured registry host (BAKE_IMAGE_REGISTRY — historically SWR, now
633
+ // docker.io/claybobby; SWR deprecated) so the bake row's repo/ref/index-id match the P1 seed
634
+ // convention (`${registry}/${profile}`); null registry ⇒ the host-less node-local name (documented
635
+ // non-pullable). For a no-push bake the digest is the build.sh LOCAL_ID (node-local, NOT selectable —
636
+ // registerBuilding writes status:'building', and latestPublished filters status='published').
466
637
  const profile = bake.profile && bake.profile !== "custom" ? bake.profile : (manifestBody.profile && manifestBody.profile !== "custom" ? manifestBody.profile : "custom");
467
638
  repo = registry ? `${registry}/${profile}` : `sema-images/${profile}`;
468
639
  manifestSha = typeof line.manifestSha === "string" ? line.manifestSha : (typeof line.manifest?.sha256 === "string" ? String(line.manifest.sha256) : bake.manifestSha);
640
+ // codex R23:身份候选(digest/repo/manifestSha)先验**后写**——node-postgres 的 UTF-8 编码会把
641
+ // lone surrogate 折成 U+FFFD,registerBuilding 可能**成功**落一条身份已变形的 index 行,随后
642
+ // 帧级 identityInvalid 折叠才拒帧 → index 已被污染且与拒帧后的 null 身份终态互相矛盾。脏身份
643
+ // ⇒ 零 index 写(indexId 强制 null),repo/manifestSha 已按本帧生效值赋出,下方折叠必然命中
644
+ // 并独占 FAILED/null 身份终态(该臂不是 register 失败,不走 partial-publish 契约/P3 补注册)。
469
645
  if (pgHasUnstorable(digest) || pgHasUnstorable(repo) || pgHasUnstorable(manifestSha)) {
470
646
  indexId = null;
471
647
  registerSteps.push({ step: "register", status: "skipped", reason: "identity fields contained unstorable bytes" });
472
648
  }
473
649
  else {
650
+ // Synthesize a `step:"register"` start around the auto-register so a resuming reader sees the register phase
651
+ // (§P2.2: the `register` step is image-api-synthesized — build.sh stops at COMPLETE + the manifest line).
474
652
  registerSteps.push({ step: "register", status: "running" });
475
653
  try {
476
654
  const entry = manifestToIndexEntry(manifestBody, { repo, digest, status: "building", manifestSha, sizeBytes: manifestBody.sizeBytes ?? null, signed: false });
655
+ // 🔴 H3: FORCE visibility/tenantId from the control plane — a P2 standard-pool bake is ALWAYS a public,
656
+ // tenant-less row. NEVER let the runner-supplied manifestBody (owner/visibility) decide the index row's
657
+ // tenancy (a zombie runner could otherwise write a `tenant`-visibility row with an attacker-chosen tenant).
477
658
  entry.visibility = "public";
478
659
  entry.tenantId = null;
660
+ // STATUS-MONOTONIC register (§P2.10 MUST-FIX): registerBuilding NEVER downgrades a `published` row — a
661
+ // late/duplicate `done` for an already-promoted digest is a no-op on the lifecycle, never a regression.
479
662
  indexId = await idx.registerBuilding(entry);
480
663
  ref = `${repo}@${digest}`;
664
+ // codex R24:setIndexId 是 non-terminal 门写(见 store 注释)——本请求悬在 registerBuilding 期间
665
+ // 另一 done(如 R23 的 dirty 折叠帧)可能已赢 setTerminal;未绑定⇒indexId 不进本帧 terminal 对象
666
+ // (本请求随后必输 CAS,不落行不落事件,index 里的孤 building 行不 publish 不可选、由生命周期兜底)。
481
667
  const bound = await bakes.setIndexId(bakeId, indexId);
482
668
  if (!bound)
483
669
  indexId = null;
484
670
  registerSteps.push({ step: "register", status: "ok" });
485
671
  }
486
672
  catch (err) {
673
+ // built-but-unregistered partial (a TiDB blip post-build): downgrade the done to FAILED, keep digest+repo
674
+ // (+ ref so the artifact is findable), leave indexId null. The P3 reconciliation tick re-registers it.
487
675
  indexId = null;
488
676
  ref = `${repo}@${digest}`;
489
677
  finalStatus = "FAILED";
490
- registerDowngraded = true;
491
- finalExit = exitCode ?? 0;
678
+ registerDowngraded = true; // M1: this FAILED is owned by the register failure, not the runner's code.
679
+ finalExit = exitCode ?? 0; // build.sh itself succeeded (exit 0); the register is the post-build failure.
492
680
  finalError = redactSecrets(`index register failed after a COMPLETE build (built-but-unregistered partial): ${String(err?.message ?? err)}`);
493
681
  registerSteps.push({ step: "register", status: "fail", reason: "index register failed (TiDB?)" });
494
682
  }
495
683
  }
496
684
  }
497
685
  }
686
+ // errorCode + coarse status are computed from the FINAL (possibly-downgraded) status, not build.sh's raw one.
687
+ // M1 (§P2.14 #2): PREFER a validated runner-supplied `line.errorCode` (the disk-guard's "disk", a future
688
+ // build-time "band-conflict" — codes the exit-code mapper cannot reconstruct) over the recompute. A `cancelled`
689
+ // terminal always wins (the kill is authoritative); a register-downgraded FAILED owns its own code (the runner's
690
+ // COMPLETE-time code is stale), so it falls back to the exit mapper.
498
691
  const suppliedCode = typeof line.errorCode === "string" && RUNNER_ERROR_CODES.has(line.errorCode)
499
692
  ? line.errorCode
500
693
  : undefined;
@@ -503,9 +696,14 @@ async function ingestBakeLine(bakes, idx, bake, line, registry) {
503
696
  : suppliedCode !== undefined && !registerDowngraded
504
697
  ? suppliedCode
505
698
  : errorCodeForExit(finalExit, cancelled);
699
+ // M1: a band-conflict terminal carries the offending band ids so the composer UI can red-highlight the chips
700
+ // (§P2.14 #2). Only honored when the FINAL code is band-conflict (a downgraded/other terminal carries none).
506
701
  const conflictBands = errorCode === "band-conflict" && Array.isArray(line.conflictBands)
507
702
  ? line.conflictBands.filter((b) => typeof b === "string")
508
703
  : null;
704
+ // codex R22:身份位(digest/repo/ref/manifestSha)带不可存字节=帧无效——在**构造前**统一折叠
705
+ // (row/SSE done/HTTP 三面同源一份 effective 对象;store 内折叠留作双保险,值恒等)。COMPLETE 折
706
+ // FAILED(null 身份的成功=谎),错误面补一句归因。
509
707
  const identityInvalid = (typeof digest === "string" && pgHasUnstorable(digest)) ||
510
708
  (typeof repo === "string" && pgHasUnstorable(repo)) ||
511
709
  (typeof ref === "string" && pgHasUnstorable(ref)) ||
@@ -516,11 +714,17 @@ async function ingestBakeLine(bakes, idx, bake, line, registry) {
516
714
  repo = null;
517
715
  ref = null;
518
716
  manifestSha = null;
519
- indexId = null;
717
+ indexId = null; // codex R23:null 身份终态不得声称 index 绑定(register 门已跳过全部 index 写)
520
718
  finalError = finalError ? `${finalError}; identity fields contained unstorable bytes` : "terminal frame identity fields contained unstorable bytes";
521
719
  }
522
720
  const finalState = (finalStatus === "COMPLETE" ? "COMPLETE" : finalStatus === "CANCELLED" ? "CANCELLED" : "FAILED");
523
721
  const coarse = coarseStatusForState(finalState);
722
+ // M3: flip the coarse terminal row FIRST and gate the terminal `done` EVENT (+ the register `step` frames) on
723
+ // WINNING that queued/running→terminal CAS. A late/contradictory terminal (cancel-vs-complete, reaper-vs-recovery)
724
+ // sees affectedRows===0 and writes NO event, so the SSE log never replays a second, conflicting terminal `done`.
725
+ // Invariant: at most one terminal `done` event per bake, always matching the coarse row outcome (§P2.14).
726
+ // L2: the flip is idempotent (`WHERE status IN ('queued','running')`), so a transient throw is safe to retry
727
+ // rather than leaving the row `running` with a `done` event the reaper would later contradict.
524
728
  let affectedRows = 0;
525
729
  const terminal = {
526
730
  status: coarse === "done" ? "done" : "failed",
@@ -547,9 +751,13 @@ async function ingestBakeLine(bakes, idx, bake, line, registry) {
547
751
  }
548
752
  }
549
753
  if (lastErr !== undefined) {
754
+ // L2: exhausted the bounded retry — DON'T 500 (safeIngest swallows it and the row would stay `running` with no
755
+ // terminal event the reaper later contradicts). Return a non-terminal ack so the runner re-POSTs the
756
+ // (lineOrd-idempotent) done to re-drive the flip.
550
757
  return { status: 200, body: { accepted: true, terminal: false, needsFlip: true } };
551
758
  }
552
759
  if (affectedRows >= 1) {
760
+ // We won the transition: replay the buffered register `step` frames, then append the single terminal `done`.
553
761
  for (const s of registerSteps)
554
762
  await bakes.appendEvent(bakeId, "step", s);
555
763
  await bakes.appendEvent(bakeId, "done", {