@sema-agent/server 7.43.0 → 7.44.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 (345) hide show
  1. package/USAGE.md +18 -0
  2. package/dist/adoption/plan.js +31 -192
  3. package/dist/adoption/quiesce.js +3 -86
  4. package/dist/adoption/runner.js +3 -140
  5. package/dist/adoption/sql.js +0 -74
  6. package/dist/adoption/wire.js +0 -80
  7. package/dist/approval-ask-machine.js +0 -75
  8. package/dist/approval-card.js +0 -323
  9. package/dist/approval-deny-reasons.js +0 -42
  10. package/dist/approval-hmac.js +0 -35
  11. package/dist/approval-reconciler.js +8 -166
  12. package/dist/approval.js +4 -67
  13. package/dist/audit.js +1 -44
  14. package/dist/auth-bridge.js +4 -47
  15. package/dist/auth-keys.js +0 -23
  16. package/dist/bake-runner/main.js +4 -65
  17. package/dist/bake-runner/protocol.js +3 -58
  18. package/dist/bake-runner/runner.js +5 -91
  19. package/dist/bench/l8/artifact.js +2 -61
  20. package/dist/bench/l8/escape.js +0 -25
  21. package/dist/bench/l8/index.js +0 -14
  22. package/dist/bench/l8/probes.js +0 -60
  23. package/dist/bench/l8/run-probes.js +2 -45
  24. package/dist/bench/s1/arms.js +14 -189
  25. package/dist/bench/s1/live-deps.js +11 -253
  26. package/dist/bench/s1/oracle.js +1 -36
  27. package/dist/bench/s1/repair-oracle-adapter.js +0 -30
  28. package/dist/bench/s1/reviewer.js +0 -34
  29. package/dist/bench/s1/row.js +0 -62
  30. package/dist/bench/s1/run-firm.js +4 -70
  31. package/dist/bench/s1/runner-ctx.js +0 -40
  32. package/dist/bench/s1/tasks.js +0 -115
  33. package/dist/boot/adoption.js +0 -20
  34. package/dist/boot/budget-tracing.js +3 -55
  35. package/dist/boot/config-center.js +43 -551
  36. package/dist/boot/coordinators.js +1 -69
  37. package/dist/boot/crash-last.js +0 -18
  38. package/dist/boot/deferred-sandbox-path-env.js +3 -119
  39. package/dist/boot/execution-env.js +5 -171
  40. package/dist/boot/governance-seams.js +0 -92
  41. package/dist/boot/leader.js +0 -69
  42. package/dist/boot/limit-sync.js +0 -2
  43. package/dist/boot/memory-boundary.js +3 -91
  44. package/dist/boot/org-memory.js +1 -25
  45. package/dist/boot/parked-revive-gate.js +0 -150
  46. package/dist/boot/permission-rules-audit.js +1 -56
  47. package/dist/boot/reapers.js +15 -273
  48. package/dist/boot/resolve-spec.js +9 -768
  49. package/dist/boot/retention-lane.js +4 -87
  50. package/dist/boot/runner-deps.js +3 -297
  51. package/dist/boot/runtime-caps.js +0 -40
  52. package/dist/boot/session-faces.js +2 -133
  53. package/dist/boot/shutdown.js +9 -83
  54. package/dist/boot/side-query-lane.js +2 -137
  55. package/dist/boot/stores.js +15 -215
  56. package/dist/boot/task-list-lane.js +0 -18
  57. package/dist/boot/webfetch-summarize-lane.js +0 -47
  58. package/dist/boot/workflow-orchestration.js +3 -99
  59. package/dist/boot-reclaim.js +0 -20
  60. package/dist/bounded-session-map.js +0 -19
  61. package/dist/brain.js +2 -139
  62. package/dist/budget.js +9 -244
  63. package/dist/capabilities/builtin-tools.js +0 -3
  64. package/dist/capabilities/center-plugins.js +6 -51
  65. package/dist/capabilities/center-prompts.js +6 -71
  66. package/dist/capabilities/code-review-council.js +7 -40
  67. package/dist/capabilities/collab-workflows.js +1 -44
  68. package/dist/capabilities/hands-lane.js +0 -65
  69. package/dist/capabilities/memory-notice.js +0 -70
  70. package/dist/capabilities/prompt.js +0 -14
  71. package/dist/capabilities/prompts/code-review.js +0 -14
  72. package/dist/capabilities/prompts/identity.js +0 -6
  73. package/dist/capabilities/prompts/team.js +0 -4
  74. package/dist/capabilities/repo-tools.js +0 -34
  75. package/dist/capabilities/sandbox-file-send.js +3 -92
  76. package/dist/capabilities/scenarios.d.ts +0 -1
  77. package/dist/capabilities/scenarios.js +18 -279
  78. package/dist/capabilities/select-environment-tool.js +0 -32
  79. package/dist/capabilities/send-user-file-tool.js +2 -66
  80. package/dist/capabilities/skills.d.ts +1 -2
  81. package/dist/capabilities/skills.js +3 -21
  82. package/dist/capabilities/team.d.ts +3 -11
  83. package/dist/capabilities/team.js +3 -51
  84. package/dist/capabilities/tool-defer.js +0 -3
  85. package/dist/config-center/apply-effective.js +15 -330
  86. package/dist/config-center/apply-ledger.js +5 -38
  87. package/dist/config-center/facade.js +0 -41
  88. package/dist/config-center/hot-keys-registry.js +0 -20
  89. package/dist/config-center/http-client.js +2 -125
  90. package/dist/config-center/mcp-revocation.js +2 -34
  91. package/dist/config-center/read-face.js +0 -59
  92. package/dist/config-center/restart-signal.js +1 -85
  93. package/dist/config-center/skills-mcp.d.ts +1 -1
  94. package/dist/config-center/skills-mcp.js +9 -76
  95. package/dist/config-center/stage-limits.js +8 -35
  96. package/dist/config-invariants.js +0 -16
  97. package/dist/config-lkg.js +0 -42
  98. package/dist/config-provider.js +3 -186
  99. package/dist/config-types.js +0 -5
  100. package/dist/config.js +107 -1145
  101. package/dist/degenerate-instrument.js +3 -67
  102. package/dist/deployment-governance.js +0 -124
  103. package/dist/digest-form.js +0 -12
  104. package/dist/elicitation.js +3 -86
  105. package/dist/env-facts.js +7 -75
  106. package/dist/fleet/fleet-bus.js +34 -507
  107. package/dist/fleet/fleet-reconciler.js +9 -149
  108. package/dist/fleet/fleet-terminal-window.js +10 -178
  109. package/dist/fleet/subagent-tail-bus.js +3 -72
  110. package/dist/fleet-client.js +10 -70
  111. package/dist/fleet-lease.js +5 -79
  112. package/dist/git-api-kind.js +0 -3
  113. package/dist/governance-ask-marks.js +2 -78
  114. package/dist/hooks/branch-transcript.js +0 -74
  115. package/dist/hooks/cc-agent-hook-prompt.js +0 -29
  116. package/dist/hooks/cc-stop-prompt.js +1 -46
  117. package/dist/hooks/hook-llm.js +1 -53
  118. package/dist/hooks/hook-runner.js +20 -414
  119. package/dist/http/active-run-conflict.js +4 -129
  120. package/dist/http/cursor-fingerprint.d.ts +5 -0
  121. package/dist/http/cursor-fingerprint.js +5 -0
  122. package/dist/http/idempotency.js +0 -37
  123. package/dist/http/principal-gate.js +3 -40
  124. package/dist/http/route-ctx.js +0 -9
  125. package/dist/http/routes/a2a-serve.js +7 -319
  126. package/dist/http/routes/admin-config-refresh.js +0 -4
  127. package/dist/http/routes/admin-drain.js +0 -10
  128. package/dist/http/routes/adoption.js +1 -29
  129. package/dist/http/routes/agents-roster.js +1 -47
  130. package/dist/http/routes/approvals-assistant.js +32 -395
  131. package/dist/http/routes/attachments.js +4 -22
  132. package/dist/http/routes/capabilities.js +3 -420
  133. package/dist/http/routes/diagnostics.js +1 -81
  134. package/dist/http/routes/fleet.js +9 -185
  135. package/dist/http/routes/images.js +14 -239
  136. package/dist/http/routes/leader.js +0 -13
  137. package/dist/http/routes/memory-bundle.js +1 -61
  138. package/dist/http/routes/memory-policy.js +9 -99
  139. package/dist/http/routes/notify-wake.js +3 -37
  140. package/dist/http/routes/observability.js +2 -19
  141. package/dist/http/routes/retention-ops.js +2 -34
  142. package/dist/http/routes/rules.js +1 -93
  143. package/dist/http/routes/runs.js +64 -873
  144. package/dist/http/routes/session-sync.js +19 -258
  145. package/dist/http/routes/sessions-list.js +17 -43
  146. package/dist/http/routes/sessions.js +34 -226
  147. package/dist/http/routes/shared-memory.js +5 -36
  148. package/dist/http/routes/side-query.js +1 -87
  149. package/dist/http/routes/tasks.js +52 -722
  150. package/dist/http/routes/trace-usage.js +32 -195
  151. package/dist/http/routes/workflows.js +19 -195
  152. package/dist/http/run-meta.js +0 -6
  153. package/dist/http/send.js +0 -32
  154. package/dist/http/server.js +92 -1597
  155. package/dist/http/sse-lifecycle.js +2 -13
  156. package/dist/http/sse-log.js +3 -48
  157. package/dist/http/tar.js +5 -21
  158. package/dist/http/verify-rounds.js +0 -5
  159. package/dist/http/wire-gate.js +0 -9
  160. package/dist/http/workspace-content.js +0 -10
  161. package/dist/images/bake-validate.js +1 -70
  162. package/dist/images/manifest.js +1 -6
  163. package/dist/index.js +0 -21
  164. package/dist/key-resolver.js +2 -17
  165. package/dist/leader/diffout.js +1 -20
  166. package/dist/leader/diffup.js +0 -47
  167. package/dist/leader/endpoint.js +2 -57
  168. package/dist/leader/fanout.js +3 -45
  169. package/dist/leader/grader-env-factory.js +3 -72
  170. package/dist/leader/leader.js +5 -156
  171. package/dist/leader/merge.js +8 -115
  172. package/dist/leader/planner.js +3 -54
  173. package/dist/leader/repair-oracle.js +1 -60
  174. package/dist/leader/repair-wire.js +2 -79
  175. package/dist/leader/wire.js +8 -307
  176. package/dist/lsp/e2b-bridge.js +4 -64
  177. package/dist/lsp/e2b-manager.js +6 -94
  178. package/dist/lsp/lsp-frames.js +0 -12
  179. package/dist/lsp/manager.js +4 -96
  180. package/dist/lsp/ws-transport.js +5 -55
  181. package/dist/lsp-evict.js +1 -15
  182. package/dist/main.js +57 -765
  183. package/dist/memory-bundle-engine.js +0 -55
  184. package/dist/memory-export.js +0 -4
  185. package/dist/memory-posture.js +1 -15
  186. package/dist/memory-scope.js +11 -148
  187. package/dist/memory-sync-client.js +2 -44
  188. package/dist/memory-sync.js +1 -80
  189. package/dist/model-select.js +3 -80
  190. package/dist/observability/cost-quota.js +1 -17
  191. package/dist/observability/cost-taxonomy.js +0 -34
  192. package/dist/observability/fail-open.js +7 -86
  193. package/dist/observability/logger.js +0 -6
  194. package/dist/observability/metrics.js +0 -94
  195. package/dist/observability/otel-exporter.js +3 -13
  196. package/dist/observability/principal-context.js +0 -9
  197. package/dist/observability/prompt-manifest.js +1 -37
  198. package/dist/observability/rate-limit.js +0 -4
  199. package/dist/observability/secret-env-scrub.js +2 -56
  200. package/dist/observability/tool-trace.js +1 -70
  201. package/dist/orchestration/hardened-vm-runner.js +4 -118
  202. package/dist/orchestration/hardened-vm-worker-runner.js +1 -26
  203. package/dist/orchestration/hardened-vm-worker.js +0 -27
  204. package/dist/orchestration/subagent-steer.js +1 -45
  205. package/dist/orchestration/workflow-agent-steer.js +1 -80
  206. package/dist/orchestration/workflow-completion-inbox.js +32 -285
  207. package/dist/orchestration/workflow-notify-journal.js +16 -259
  208. package/dist/org-memory-admission.js +3 -47
  209. package/dist/parent-watch.js +2 -48
  210. package/dist/parked-decide.js +1 -109
  211. package/dist/per-task-image.js +0 -57
  212. package/dist/plan-cache-probe.js +3 -27
  213. package/dist/plugins/adoption-log-sql.js +2 -119
  214. package/dist/plugins/approval-ask-store-memory.js +3 -38
  215. package/dist/plugins/approval-ask-store-sql.js +6 -188
  216. package/dist/plugins/approval-exemption-store.js +2 -28
  217. package/dist/plugins/background-agent-store-sql.js +4 -105
  218. package/dist/plugins/background-shell-support.js +14 -122
  219. package/dist/plugins/blob-backend.js +6 -169
  220. package/dist/plugins/breaker-state-sql.js +8 -46
  221. package/dist/plugins/caching-session-store.js +4 -106
  222. package/dist/plugins/checkpoint-store-sql.js +18 -505
  223. package/dist/plugins/e2b-orphan-reclaim.js +0 -45
  224. package/dist/plugins/file-outcome-sink.js +0 -9
  225. package/dist/plugins/file-resume-anchor-store.js +4 -43
  226. package/dist/plugins/file-run-store.js +26 -364
  227. package/dist/plugins/file-snapshot-store-sql.js +10 -181
  228. package/dist/plugins/fork-routing-session-store.js +8 -111
  229. package/dist/plugins/host-platform.js +2 -91
  230. package/dist/plugins/image-bake-store-sql.js +7 -250
  231. package/dist/plugins/image-index-sql.js +4 -123
  232. package/dist/plugins/k8s-bg-scripts.js +4 -88
  233. package/dist/plugins/k8s-exec-protocol.js +0 -41
  234. package/dist/plugins/leader-run-store-sql.js +0 -101
  235. package/dist/plugins/local-checkpoint-store.js +8 -128
  236. package/dist/plugins/local-session-store.js +34 -296
  237. package/dist/plugins/local-task-attachment-store.js +2 -16
  238. package/dist/plugins/mailbox-store-sql.js +8 -65
  239. package/dist/plugins/memory-embedder-fingerprint.js +5 -166
  240. package/dist/plugins/memory-embedder.js +4 -70
  241. package/dist/plugins/memory-engine-pg.js +6 -170
  242. package/dist/plugins/memory-engine-tidb.js +7 -155
  243. package/dist/plugins/memory-engine-vector-util.js +0 -10
  244. package/dist/plugins/memory-key-guards.js +0 -34
  245. package/dist/plugins/memory-origin-law.js +0 -187
  246. package/dist/plugins/memory-resume-anchor-store.js +0 -17
  247. package/dist/plugins/memory-run-store.js +12 -89
  248. package/dist/plugins/memory-session-policy-store.js +0 -17
  249. package/dist/plugins/memory-sync-store-pg.js +4 -49
  250. package/dist/plugins/memory-sync-store-tidb.js +3 -35
  251. package/dist/plugins/outcome-ledger-sql.js +3 -97
  252. package/dist/plugins/permission-rule-store-file.js +5 -133
  253. package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
  254. package/dist/plugins/permission-rule-store-sql.js +50 -307
  255. package/dist/plugins/pg-cost-quota.js +0 -7
  256. package/dist/plugins/pg-pool.js +0 -92
  257. package/dist/plugins/pg-rate-limiter.js +2 -13
  258. package/dist/plugins/pg-safe-json.js +4 -40
  259. package/dist/plugins/pg-session-storage.js +25 -189
  260. package/dist/plugins/posix-shell-fs.js +1 -31
  261. package/dist/plugins/remote-env-adb.js +12 -101
  262. package/dist/plugins/remote-env-e2b.js +40 -371
  263. package/dist/plugins/remote-env-file-error.js +0 -37
  264. package/dist/plugins/remote-env-host.js +63 -483
  265. package/dist/plugins/remote-env-k8s.js +32 -326
  266. package/dist/plugins/remote-env-local-docker.js +24 -157
  267. package/dist/plugins/remote-env-ssh.js +20 -128
  268. package/dist/plugins/remote-scratchpad.js +2 -32
  269. package/dist/plugins/remote-shell.js +1 -32
  270. package/dist/plugins/resume-anchor-store-sql.js +0 -11
  271. package/dist/plugins/retention-lane-store-sql.js +0 -108
  272. package/dist/plugins/retention-store-sql.js +4 -383
  273. package/dist/plugins/roster-store-sql.js +0 -55
  274. package/dist/plugins/run-store-sql.js +14 -251
  275. package/dist/plugins/s3-presign.js +2 -49
  276. package/dist/plugins/scheduler-support.js +3 -80
  277. package/dist/plugins/send-file-ledger.js +4 -53
  278. package/dist/plugins/send-user-file.js +4 -94
  279. package/dist/plugins/session-placement.js +1 -89
  280. package/dist/plugins/session-policy-store-sql.js +3 -81
  281. package/dist/plugins/session-store.js +0 -57
  282. package/dist/plugins/shared-memory-store-sql.js +4 -167
  283. package/dist/plugins/sql-driver.js +0 -17
  284. package/dist/plugins/sql-errors.js +0 -7
  285. package/dist/plugins/sql-escape.js +0 -8
  286. package/dist/plugins/sql-row-helpers.js +0 -25
  287. package/dist/plugins/store-backend.js +42 -222
  288. package/dist/plugins/store-contracts.js +2 -46
  289. package/dist/plugins/task-attachment-store.js +3 -49
  290. package/dist/plugins/task-list-store-sql.js +0 -79
  291. package/dist/plugins/tidb-cost-quota.js +1 -4
  292. package/dist/plugins/tidb-pool.js +1 -207
  293. package/dist/plugins/tidb-rate-limiter.js +3 -9
  294. package/dist/plugins/tidb-session-storage.js +4 -70
  295. package/dist/plugins/tidb-session-store.js +19 -341
  296. package/dist/plugins/tool-result-store-sql.js +4 -179
  297. package/dist/plugins/usage-window-store-sql.js +0 -8
  298. package/dist/plugins/web-search.js +10 -132
  299. package/dist/plugins/workflow-journal-store-sql.js +2 -58
  300. package/dist/plugins/workflow-run-store-sql.js +6 -91
  301. package/dist/plugins/worktree-isolation.js +6 -126
  302. package/dist/plugins/write-behind-counter.js +16 -75
  303. package/dist/principal-jwt.js +5 -60
  304. package/dist/project-memory.js +15 -146
  305. package/dist/prompts-domain-validate.js +1 -51
  306. package/dist/question.js +2 -111
  307. package/dist/resource-suspend.js +0 -18
  308. package/dist/router/route-orchestration.js +0 -77
  309. package/dist/rules-consent.d.ts +44 -5
  310. package/dist/rules-consent.js +75 -215
  311. package/dist/run-local.js +14 -381
  312. package/dist/runs.js +29 -617
  313. package/dist/runtime-caps-resolver.js +6 -132
  314. package/dist/runtime-governance.js +1 -232
  315. package/dist/sandbox-pkg-source.js +0 -37
  316. package/dist/sealed-key.js +3 -68
  317. package/dist/security.js +6 -291
  318. package/dist/session-leaf-bus.js +0 -32
  319. package/dist/session-sync-content.js +1 -69
  320. package/dist/session-sync-kernel.js +3 -60
  321. package/dist/session-sync.js +3 -66
  322. package/dist/session-titler.js +7 -41
  323. package/dist/session-watch.js +9 -73
  324. package/dist/shared-memory-scope-authorizer.js +0 -18
  325. package/dist/sighup-idle.js +1 -12
  326. package/dist/spec-fields.js +4 -143
  327. package/dist/store-live-probe.js +3 -49
  328. package/dist/task-a2a.js +1 -126
  329. package/dist/task-cwd.js +1 -103
  330. package/dist/task-mcp.js +1 -89
  331. package/dist/task-settings.js +10 -300
  332. package/dist/task-workflow.js +8 -75
  333. package/dist/tool-approval.d.ts +28 -1
  334. package/dist/tool-approval.js +70 -1430
  335. package/dist/trace/artifacts.js +5 -20
  336. package/dist/trace/engine-notice-wire.js +6 -138
  337. package/dist/trace/ledger-sink.js +7 -96
  338. package/dist/trace/project.js +4 -448
  339. package/dist/trace/redact.js +9 -77
  340. package/dist/turn-activity.js +1 -27
  341. package/dist/usage-analytics.js +4 -34
  342. package/dist/wall-clock-jump-guard.js +1 -68
  343. package/package.json +3 -3
  344. package/dist/capabilities/scenario-alias.d.ts +0 -27
  345. package/dist/capabilities/scenario-alias.js +0 -61
@@ -8,14 +8,8 @@ import { sendJson, sendError } from "../send.js";
8
8
  import { headerStr, safeEqual, gatedPrincipal, explicitOperatorOk } 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).
15
11
  return { bakeSubmitLimiter: new RateLimiter(deps.config.imageBakes?.submitRateMax ?? 0, (deps.config.imageBakes?.submitRateWindowSec ?? 60) * 1000) };
16
12
  }
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`). */
19
13
  export function coarseStatusForState(state) {
20
14
  if (state === "COMPLETE")
21
15
  return "done";
@@ -23,46 +17,29 @@ export function coarseStatusForState(state) {
23
17
  return "failed";
24
18
  return "running";
25
19
  }
26
- /** [2373]C-7:202 受理体的**唯一铸点**——此前四份逐字构造散在 attach/竞态重读/新建/dryRun 四臂,
27
- * 下一个字段必然只加到其中两处(:213/:217 与 :193/:206 已有分叉风险)。status/state 双发保留
28
- * (status=state 的粗化像,省客户端抄映射表)。 */
29
20
  function bakeAcceptedBody(b) {
30
21
  return { bakeId: b.bakeId, eventsUrl: `/v1/images/bakes/${b.bakeId}/events`, status: b.status, state: b.state };
31
22
  }
32
- /** build.sh's structured exit code → the center band-composer UI's terminal `errorCode` (§P2.14 #2). null for an
33
- * uncategorized failure (then `error` free-text + `exitCode` drive the UI). 10=dup-tag, 124=timeout, 507=disk. */
34
23
  export function errorCodeForExit(exitCode, cancelled) {
35
24
  if (cancelled)
36
25
  return "cancelled";
37
26
  switch (exitCode) {
38
27
  case 9:
39
- return "band-conflict"; // build.sh's generate-time inter-band collision (gen-dockerfile exit 6/7/8 → 9).
28
+ return "band-conflict";
40
29
  case 10:
41
30
  return "duplicate";
42
31
  case 124:
43
32
  return "timeout";
44
33
  case 507:
45
- return "disk"; // belt: the runner's pre-flight disk guard (runner.ts) exits 507 — categorize it even if the
46
- // structured line.errorCode were ever absent (M1: line.errorCode is the primary source).
34
+ return "disk";
47
35
  default:
48
36
  return null;
49
37
  }
50
38
  }
51
- /** The closed set of structured terminal `errorCode`s a runner/build.sh frame may carry (§P2.14 #2). A
52
- * runner-supplied code is a UI hint only, but it CANNOT reconstruct from the exit code alone (e.g. the disk
53
- * guard's 507, or a future band-conflict 6/7/8), so ingestBakeLine PREFERS a validated `line.errorCode` over
54
- * the exit-code recompute. Set-membership keeps it inside the BakeErrorCode union (never trust a free-text). */
55
39
  const RUNNER_ERROR_CODES = new Set(["band-conflict", "duplicate", "disk", "timeout", "cancelled"]);
56
- /** The index `profile` under which the FROM base (`sandbox-base`) is published — the ONLY profile a bake's
57
- * `--base-ref` may resolve to (a published code-* profile digest is a built layer, never a valid FROM base).
58
- * Both the caller-supplied-baseRef allow-list AND the default-base fallback resolve against this profile. */
59
40
  const SANDBOX_BASE_PROFILE = "sandbox-base";
60
- // sandbox-image-pool query routes (IMAGE-API-DESIGN.md §2). `/digests/:digest` is checked BEFORE `/:profile`
61
- // (the single-segment profile RE would otherwise swallow "digests"); both are GET-only + visibility-scoped.
62
41
  export const IMAGE_DIGEST_RE = /^\/v1\/images\/digests\/([^/]+)$/;
63
42
  export const IMAGE_PROFILE_RE = /^\/v1\/images\/([^/]+)$/;
64
- // sandbox-image-pool BAKE routes (IMAGE-API-DESIGN.md §P2.1). No trailing-slash variant (`/v1/images/bakes/` →
65
- // 404, matching the live `/v1/images/` behavior). The `:bakeId` group + sub-action are both-or-neither.
66
43
  export const BAKE_ID_RE = /^\/v1\/images\/bakes\/([^/]+)$/;
67
44
  export const BAKE_EVENTS_RE = /^\/v1\/images\/bakes\/([^/]+)\/events$/;
68
45
  export const BAKE_CANCEL_RE = /^\/v1\/images\/bakes\/([^/]+)\/cancel$/;
@@ -73,40 +50,24 @@ export async function handleImages(req, res, url, ctx) {
73
50
  await handleImagesBody(req, res, url, ctx, miss);
74
51
  return !miss.fell;
75
52
  }
76
- /** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。裸 `return;`
77
- * 的语义原样 =「本域已应答」;走到函数尾才是「没匹配上」,置 `miss.fell` 交回装配器继续下一个域。
78
- * 跨域的东西(deps / 助手 / 单域状态 / 每请求量)一律在首行解构出来,于是下面每一行都逐字不变。 */
79
53
  async function handleImagesBody(req, res, url, ctx, miss) {
80
54
  const { deps } = ctx;
81
55
  const { readJson } = ctx.helpers;
82
56
  const { source } = ctx.req;
83
57
  const { bakeSubmitLimiter } = ctx.local.images;
84
- // ── sandbox-image-pool BAKE control plane (IMAGE-API-DESIGN.md §P2). MUST precede the P1 images block: the P1
85
- // profile RE `/^\/v1\/images\/([^/]+)$/` would otherwise swallow `/v1/images/bakes` as profile="bakes" (404),
86
- // and a two-segment `/v1/images/bakes/:id` would fall through P1 with NO response (hung socket). image-api is
87
- // the UNPRIVILEGED controller: it never bakes — it owns the durable job row/event log/lease, the operator gate,
88
- // and the input whitelist; the privileged build runs on the bake-runner (build-host) over the internal claim/ingest.
89
58
  if (deps.imageBakes && url.startsWith("/v1/images/bakes")) {
90
59
  const bakes = deps.imageBakes;
91
- const idx = deps.imageIndex; // the auto-register target (wired together with imageBakes in main.ts)
92
- // Identity resolved EXACTLY as P1 (direct-door JWT-without-cnf.bnd, else principalFrom).
93
- const principal = gatedPrincipal(req, deps.config); // direct-door safe — identity-only proof; single source of truth (see gatedPrincipal)
60
+ const idx = deps.imageIndex;
61
+ const principal = gatedPrincipal(req, deps.config);
94
62
  const operators = deps.config.operatorPrincipals;
95
63
  const isExplicitOperator = explicitOperatorOk(principal, operators);
96
64
  const cfg = deps.config.imageBakes;
97
- // 🔴 H1: the bake-runner identity is DERIVED from its bearer CREDENTIAL (the token-derived `source`,
98
- // resolved above from SERVICE_AUTH_TOKENS), NOT a self-asserted x-agent-principal/JWT the runner never
99
- // sends. `source` is independent of the directDoorActive principal ternary, so this works identically in
100
- // direct-door AND plain mode. The config boot invariant guarantees a runner-credential→runnerPrincipal
101
- // mapping exists when bakes are enabled.
102
65
  const isRunner = source !== undefined && source !== null && source === cfg.runnerPrincipal;
103
- // POST /v1/images/bakes — operator-only submit. EXPLICIT operator gate (NOT the bare isOperator; §P2.4a).
104
66
  if (req.method === "POST" && url === "/v1/images/bakes") {
105
67
  if (!isExplicitOperator) {
106
68
  sendError(res, 403, "auth.operator_only", "operator only");
107
69
  return;
108
70
  }
109
- // Per-principal submission rate guard (§P2.4c) — caps queue-churn even though the lease caps live builds at 1.
110
71
  const rl = bakeSubmitLimiter.check(`bake:${principal}`);
111
72
  if (!rl.allowed) {
112
73
  res.setHeader("retry-after", String(rl.retryAfterSec));
@@ -114,22 +75,11 @@ async function handleImagesBody(req, res, url, ctx, miss) {
114
75
  return;
115
76
  }
116
77
  const body = (await readJson(req));
117
- // Input WHITELIST — set-membership BEFORE any argv is built (§P2.4d). Off-list → 400/501, never coerced.
118
78
  const v = validateBake(body);
119
- // B7:errorCode 恒在——band 冲突保留既有 `band-conflict` 机器码(1.x 契约,勿改);其余 400=字段白名单
120
- // 拒收、501=该形 bake 尚未开面(custom/tenant P4)。`error` 文案与键序逐字不变。
121
79
  if (!v.ok) {
122
80
  sendError(res, v.status, v.conflictBands ? "band-conflict" : v.status === 501 ? "capability.unavailable" : "request.field_invalid", v.error, { ...(v.conflictBands ? { conflictBands: v.conflictBands } : {}) });
123
81
  return;
124
82
  }
125
- // baseRef allow-list (§P2.4c): a caller CANNOT pass an arbitrary FROM. A present baseRef must be a KNOWN
126
- // PUBLISHED `sandbox-base` digest the index has seen (a syntactically-valid attacker digest is still
127
- // arbitrary; a published code-* PROFILE digest is also not a valid FROM base — the profile must be
128
- // sandbox-base). Absent ⇒ the pinned default base = `SANDBOX_BASE_REF` OR the index's latest published
129
- // sandbox-base (design §P2.4c "SANDBOX_BASE_REF / the index's latest published base"). The shape was
130
- // already checked in validateBake. The digest the server resolves is the SAME for push:true and
131
- // push:false — a node-local (no-push) build's host docker resolves a sandbox-base@sha256 FROM against
132
- // the locally-present base image by its config digest (verified live), so one digest serves both paths.
133
83
  let effectiveBaseRef;
134
84
  if (v.baseRef) {
135
85
  const baseEntry = idx ? await idx.getByDigest(v.baseRef) : null;
@@ -140,25 +90,15 @@ async function handleImagesBody(req, res, url, ctx, miss) {
140
90
  effectiveBaseRef = v.baseRef;
141
91
  }
142
92
  else {
143
- // SANDBOX_BASE_REF pin first; else the index's latest published sandbox-base (operator viewer — the base
144
- // pool is server-internal, never tenant-scoped). The server still WHOLLY chooses the base (the caller
145
- // never influences it), so the anti-RCE property holds.
146
93
  effectiveBaseRef = cfg.defaultBaseRef ?? (idx ? (await idx.latestPublished(SANDBOX_BASE_PROFILE, { operator: true }))?.digest ?? null : null);
147
94
  }
148
- // A real (non-dryRun) build MUST FROM a concrete base — build.sh dies (exit 3) on a missing --base-ref.
149
- // Fail CLOSED here with an honest config error rather than queue an argv that can never build (a dryRun is
150
- // resolve-only and legitimately previews with build.sh's floating base, so it's exempt).
151
95
  if (!v.dryRun && !effectiveBaseRef) {
152
96
  sendError(res, 503, "state.no_sandbox_base", "no sandbox-base available to build from (set SANDBOX_BASE_REF or publish a sandbox-base image)");
153
97
  return;
154
98
  }
155
99
  const argv = buildBakeArgv(v, { baseRef: effectiveBaseRef, cacheBase: cfg.cacheBase });
156
100
  const rawIdem = headerStr(req.headers["idempotency-key"]) ?? (typeof body.idempotencyKey === "string" ? body.idempotencyKey : undefined);
157
- // Durable submit-idempotency: scope the key to the submitting principal (BL-3 parity) so a replay is the
158
- // SAME caller only; the store's `idem_key` UNIQUE is the durable dedupe.
159
101
  const idemKey = rawIdem ? scopedIdempotencyKey(rawIdem, source, principal) ?? null : null;
160
- // §P2.1/§P2.7 single-flight at SUBMIT (concurrency 1; no auto-queue). A `dryRun` is resolve-only — it never
161
- // takes the lease, so it bypasses the busy guard entirely (parallel-safe) and uses the plain createBake.
162
102
  const createInput = {
163
103
  profile: v.profile,
164
104
  bands: v.bands,
@@ -171,18 +111,11 @@ async function handleImagesBody(req, res, url, ctx, miss) {
171
111
  requestedBy: principal ?? null,
172
112
  };
173
113
  if (!v.dryRun) {
174
- // (a) an IDENTICAL-argv in-flight bake ⇒ attach (idempotent re-submit, 202) — no second build.
175
114
  const sameArgv = await bakes.findActiveByArgv(argv);
176
115
  if (sameArgv) {
177
116
  sendJson(res, 202, bakeAcceptedBody(sameArgv));
178
117
  return;
179
118
  }
180
- // (b) M4: ATOMIC admission — the busy-check and the create are ONE DB statement (`INSERT … SELECT …
181
- // WHERE NOT EXISTS (a non-dryRun in-flight bake)`), so two concurrent keyless POSTs can never both queue
182
- // (the old findActiveAny()→createBake() check-then-act interleaved). On `created:false` a DIFFERENT bake
183
- // holds (or will hold) the single-flight lease ⇒ re-read the active bake. Do NOT silently queue a 45-min
184
- // build; surface the active bake + its events URL so the UI attaches to the live one (or attach if a
185
- // concurrent identical-argv submit won the race in the gap).
186
119
  const r = await bakes.createBakeIfIdle(createInput);
187
120
  if (!r.created) {
188
121
  const sameNow = await bakes.findActiveByArgv(argv);
@@ -201,7 +134,6 @@ async function handleImagesBody(req, res, url, ctx, miss) {
201
134
  sendJson(res, 202, bakeAcceptedBody(bake));
202
135
  return;
203
136
  }
204
- // GET /v1/images/bakes/:bakeId/events — resumable SSE (the live "baking…" view), run-trace-SSE-shaped (§P2.2).
205
137
  const evMatch = req.method === "GET" ? BAKE_EVENTS_RE.exec(url) : null;
206
138
  if (evMatch) {
207
139
  if (!isExplicitOperator) {
@@ -217,7 +149,6 @@ async function handleImagesBody(req, res, url, ctx, miss) {
217
149
  await streamBakeEvents(req, res, bakes, bakeId, cfg.staleMs);
218
150
  return;
219
151
  }
220
- // POST /v1/images/bakes/:bakeId/cancel — cooperative cancel (durable flag; the runner kills the pgid, §P2.11).
221
152
  const cancelMatch = req.method === "POST" ? BAKE_CANCEL_RE.exec(url) : null;
222
153
  if (cancelMatch) {
223
154
  if (!isExplicitOperator) {
@@ -230,39 +161,31 @@ async function handleImagesBody(req, res, url, ctx, miss) {
230
161
  sendError(res, 404, "not_found.bake", "bake not found");
231
162
  return;
232
163
  }
233
- const flagged = await bakes.requestCancel(bakeId); // only flags a non-terminal row
164
+ const flagged = await bakes.requestCancel(bakeId);
234
165
  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" });
235
166
  return;
236
167
  }
237
- // POST /v1/images/bakes/claim — RUNNER-ONLY: long-poll claim (§P2.12). Find the oldest queued bake and claim
238
- // it in one call (the discovery + the single-flight CAS). Checked BEFORE the per-bake `…/:id/claim` so the
239
- // bare `claim` is not swallowed as `:bakeId="claim"`. 204 when the queue is empty (the runner idle-polls).
240
168
  if (req.method === "POST" && url === "/v1/images/bakes/claim") {
241
169
  if (!isRunner) {
242
170
  sendError(res, 403, "auth.bake_runner_only", "bake-runner only");
243
171
  return;
244
172
  }
245
173
  const body = (await readJson(req));
246
- // H1: the runner is credential-authed (no x-agent-principal), so `principal` may be undefined — fall back
247
- // to the credential-derived `source` (== runnerPrincipal, isRunner guaranteed it) as the runner identity.
248
174
  const runnerId = typeof body.runnerId === "string" && body.runnerId.length > 0 ? body.runnerId : (principal ?? source ?? cfg.runnerPrincipal);
249
- // Bounded find-then-CAS: another runner racing us may win the id we just read, so retry the next queued id
250
- // a few times before giving up (the queue is normally short + replicas=1, so 1 pass is the common case).
251
175
  let claimed = null;
252
176
  for (let attempt = 0; attempt < 4 && !claimed; attempt++) {
253
177
  const nextId = await bakes.findNextQueuedId();
254
178
  if (!nextId)
255
- break; // queue empty
179
+ break;
256
180
  claimed = await bakes.claimBake(nextId, runnerId, cfg.leaseMs);
257
181
  }
258
182
  if (!claimed) {
259
183
  res.writeHead(204).end();
260
184
  return;
261
- } // nothing claimable right now (the runner idle-polls)
185
+ }
262
186
  sendJson(res, 200, claimResponse(claimed));
263
187
  return;
264
188
  }
265
- // POST /v1/images/bakes/:bakeId/claim — RUNNER-ONLY: single-flight lease of THIS queued bake (§P2.7).
266
189
  const claimMatch = req.method === "POST" ? BAKE_CLAIM_RE.exec(url) : null;
267
190
  if (claimMatch) {
268
191
  if (!isRunner) {
@@ -274,16 +197,12 @@ async function handleImagesBody(req, res, url, ctx, miss) {
274
197
  const runnerId = typeof body.runnerId === "string" && body.runnerId.length > 0 ? body.runnerId : (principal ?? source ?? cfg.runnerPrincipal);
275
198
  const claimed = await bakes.claimBake(bakeId, runnerId, cfg.leaseMs);
276
199
  if (!claimed) {
277
- // Lost the single-flight (already claimed, or the pool lease is held by a live build) → 409.
278
200
  sendError(res, 409, "conflict.bake_not_claimable", "bake not claimable (already claimed or the build lease is held)");
279
201
  return;
280
202
  }
281
- // Return the VETTED argv + the per-bake ingest secret (minted at claim) the runner echoes on ingest.
282
203
  sendJson(res, 200, claimResponse(claimed));
283
204
  return;
284
205
  }
285
- // POST /v1/images/bakes/:bakeId/ingest — RUNNER-ONLY: append one build.sh line + heartbeat the lease.
286
- // Maps build.sh `{event}` → an SSE kind + the coarse status; on a COMPLETE `done` triggers auto-register.
287
206
  const ingestMatch = req.method === "POST" ? BAKE_INGEST_RE.exec(url) : null;
288
207
  if (ingestMatch) {
289
208
  if (!isRunner) {
@@ -296,16 +215,12 @@ async function handleImagesBody(req, res, url, ctx, miss) {
296
215
  sendError(res, 404, "not_found.bake", "bake not found");
297
216
  return;
298
217
  }
299
- // Per-bake ingest secret (minted at claim) — so a stale/rogue runner can't inject frames into another bake.
300
218
  const presented = headerStr(req.headers["x-bake-ingest-secret"]);
301
219
  if (!bake.ingestSecret || !presented || !safeEqual(presented, bake.ingestSecret)) {
302
220
  sendError(res, 403, "auth.ingest_secret_invalid", "invalid ingest secret");
303
221
  return;
304
222
  }
305
223
  const line = (await readJson(req));
306
- // A `heartbeat` frame (§P2.7/§P2.11): advance the lease, append NO event, and echo the durable cancel flag +
307
- // lease validity so the runner kills the build.sh process group at the next tick. A lost lease (the reaper
308
- // stole it as stale, or a terminal flip released it) → 409 so the runner aborts (it no longer owns the build).
309
224
  if (line.event === "heartbeat") {
310
225
  const fresh = await bakes.getBake(bakeId);
311
226
  if (!fresh || fresh.status !== "running") {
@@ -320,17 +235,11 @@ async function handleImagesBody(req, res, url, ctx, miss) {
320
235
  sendJson(res, 200, { accepted: true, leaseValid: true, cancelRequested: fresh.cancelRequested });
321
236
  return;
322
237
  }
323
- // 🔴 H3 (zombie/reaped runner) — PRIMARY guard: a terminal/reaped bake accepts NO frames. Re-read the row
324
- // (the `bake` fetched above may be stale) and reject 409 for ALL non-heartbeat frames when it is no longer
325
- // running, mirroring the heartbeat branch. Otherwise a zombie/duplicate runner that still holds a valid
326
- // ingest_secret could inject `building` index rows + out-of-band SSE frames into a dead bake's stream.
327
238
  const fresh = await bakes.getBake(bakeId);
328
239
  if (!fresh || fresh.status !== "running") {
329
240
  sendJson(res, 409, { accepted: false, leaseValid: false, cancelRequested: fresh?.cancelRequested ?? false, errorCode: "conflict.bake_not_running" });
330
241
  return;
331
242
  }
332
- // A real build.sh frame: also advance the lease (the runner heartbeats via its frame cadence too) and echo
333
- // the cancel flag so a cancel observed between heartbeats still reaches the runner promptly.
334
243
  if (fresh.runnerId) {
335
244
  await bakes.heartbeatLease(bakeId, fresh.runnerId, cfg.leaseMs).catch(() => undefined);
336
245
  }
@@ -339,8 +248,6 @@ async function handleImagesBody(req, res, url, ctx, miss) {
339
248
  sendJson(res, result.status, { ...result.body, cancelRequested: after?.cancelRequested ?? false, leaseValid: true });
340
249
  return;
341
250
  }
342
- // GET /v1/images/bakes/:bakeId — poll fallback, `{bake:}` envelope, null-not-omitted (§P2.1). Checked LAST so
343
- // the `/events|/cancel|/claim|/ingest` sub-actions (two-segment) win first.
344
251
  const idMatch = req.method === "GET" ? BAKE_ID_RE.exec(url) : null;
345
252
  if (idMatch) {
346
253
  if (!isExplicitOperator) {
@@ -356,25 +263,15 @@ async function handleImagesBody(req, res, url, ctx, miss) {
356
263
  sendJson(res, 200, { bake: bakeView(bake) });
357
264
  return;
358
265
  }
359
- // No trailing-slash variant, no other bake route → 404 (parity with `/v1/images/` live behavior).
360
266
  sendError(res, 404, "not_found.route", "not found");
361
267
  return;
362
268
  }
363
- // ── sandbox-image-pool control API (IMAGE-API-DESIGN.md §2, pool design §8) — P1: query + select. The image
364
- // INDEX is queried/selected here (bake/publish = P2/P3, later). Visibility is viewer-scoped (Codex A1): a
365
- // tenant principal sees `public` + its OWN tenant images; an operator sees all. Identity is resolved the same
366
- // way as the approval reads (direct-door JWT-without-cnf.bnd, else principalFrom); an absent principal still
367
- // sees the public catalog (anonymous public-pull parity — pool = docker.io/claybobby; SWR deprecated) but no tenant rows.
368
269
  if (deps.imageIndex && url.startsWith("/v1/images")) {
369
270
  const idx = deps.imageIndex;
370
- const principal = gatedPrincipal(req, deps.config); // direct-door safe — identity-only proof; single source of truth (see gatedPrincipal)
371
- // explicitOperatorOk (NOT isOperator): for image VISIBILITY, an empty OPERATOR_PRINCIPALS must NOT make every
372
- // caller an operator (that would leak tenant-scoped images). Adversarial-review HIGH — same fix as the P0.5
373
- // per-task resolve in main.ts. (Approval operator gates below keep isOperator's legacy empty=all boundary.)
271
+ const principal = gatedPrincipal(req, deps.config);
374
272
  const operator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
375
273
  const viewer = { operator, tenantId: principal ?? null };
376
274
  const q = new URL(req.url ?? "", "http://x").searchParams;
377
- // GET /v1/images — the paginated catalog. ?profile=&status=&capability=&latest=true&limit=&cursor=
378
275
  if (req.method === "GET" && url === "/v1/images") {
379
276
  const limRaw = q.get("limit");
380
277
  const { entries, nextCursor } = await idx.list({
@@ -389,9 +286,6 @@ async function handleImagesBody(req, res, url, ctx, miss) {
389
286
  sendJson(res, 200, { images: entries, nextCursor });
390
287
  return;
391
288
  }
392
- // POST /v1/images/select — the §7 per-sandbox resolution: profile → newest published visible digest +
393
- // podContract, with capability admission (Codex B3). Pure resolution (no build); the caller threads the
394
- // returned `ref` (repo@digest) into the kata adapter's per-sandbox image (today a worker-global env).
395
289
  if (req.method === "POST" && url === "/v1/images/select") {
396
290
  const body = (await readJson(req));
397
291
  const profile = typeof body.profile === "string" ? body.profile : "";
@@ -424,12 +318,7 @@ async function handleImagesBody(req, res, url, ctx, miss) {
424
318
  });
425
319
  return;
426
320
  }
427
- // POST /v1/images/register — operator-only ingest (P1: seed from the live bake manifests; P3 atomic-publish
428
- // drives it for real). Minimal validation here; the publish pipeline supplies a full manifest projection.
429
321
  if (req.method === "POST" && url === "/v1/images/register") {
430
- // EXPLICIT operator gate (§P2.4a MUST-FIX, the P1 follow-up): NOT the bare `operator`/isOperator, whose
431
- // empty-OPERATOR_PRINCIPALS "true-for-all" makes this index-mutating door world-writable. (The block-level
432
- // `operator` stays for read visibility; the MUTATION must require a concrete configured operator.)
433
322
  if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
434
323
  sendError(res, 403, "auth.operator_only", "operator only");
435
324
  return;
@@ -439,11 +328,6 @@ async function handleImagesBody(req, res, url, ctx, miss) {
439
328
  sendError(res, 400, "request.field_invalid", "profile, repo, digest are required");
440
329
  return;
441
330
  }
442
- // 🔴 STATUS-MONOTONIC register (adversarial-review HIGH finding): the unconditional `upsert` let an
443
- // operator POST status:'building' for an already-`published` digest and SILENTLY DE-PUBLISH a live image
444
- // (latestPublished then 404s → sandbox-create fails — the P2.10 bug class, LIVE on this door). registerBuilding
445
- // never downgrades a published row (building→published still works; published→building is a no-op on the
446
- // lifecycle). A deliberate take-down is the explicit deprecate path (P3), never a side effect of register.
447
331
  const id = await idx.registerBuilding({
448
332
  profile: b.profile,
449
333
  bands: Array.isArray(b.bands) ? b.bands : [],
@@ -467,7 +351,6 @@ async function handleImagesBody(req, res, url, ctx, miss) {
467
351
  sendJson(res, 200, { id });
468
352
  return;
469
353
  }
470
- // GET /v1/images/digests/:digest — one entry by immutable digest (what a running sandbox was built from).
471
354
  const digMatch = req.method === "GET" ? IMAGE_DIGEST_RE.exec(url) : null;
472
355
  if (digMatch) {
473
356
  const entry = await idx.getByDigest(decodeURIComponent(digMatch[1]));
@@ -478,7 +361,6 @@ async function handleImagesBody(req, res, url, ctx, miss) {
478
361
  sendJson(res, 200, { image: entry });
479
362
  return;
480
363
  }
481
- // GET /v1/images/:profile — the newest published visible entry for a profile (latest tag + versions).
482
364
  const profMatch = req.method === "GET" ? IMAGE_PROFILE_RE.exec(url) : null;
483
365
  if (profMatch) {
484
366
  const entry = await idx.latestPublished(decodeURIComponent(profMatch[1]), viewer);
@@ -491,28 +373,11 @@ async function handleImagesBody(req, res, url, ctx, miss) {
491
373
  }
492
374
  }
493
375
  else if (url === "/v1/images" || url.startsWith("/v1/images/")) {
494
- // 🔴 #F1([3397]-5 同族缺口):索引缺席时**整个域不挂载**,请求此前一路落到全局路由表尾的
495
- // `404 not_found.route` —— 与「路由拼错了」同一个码,消费端结构上分不出「这个部署没接这个面」
496
- // 与「我 URL 写错了」(trial-by-404 的最坏形:两种成因的处置完全相反)。改成诚实的能力拒。
497
- //
498
- // 谓词与上面那条挂载合取式**同一个符号**(`deps.imageIndex`),所以 `/v1/capabilities` 的 `images`
499
- // 位(同一谓词)说 yes ⟺ 这条臂不触发 ⟺ 目录读面真能用。
500
- //
501
- // ⚠️ 判据是**按段**的(`=== "/v1/images"` 或 `"/v1/images/"` 前缀),不是上面挂载式那个裸前缀:
502
- // `/v1/imagesfoo` 是**拼错的路由**,不是本域的路径,它该继续拿 404(索引在场的部署上它本来就落 404
503
- // —— 两种部署形对同一个手滑给出不同的码,那正是本臂要消灭的那种含糊)。
504
- //
505
- // ⚠️ bake 子面**不误伤**:`deps.imageBakes` 在场时 `/v1/images/bakes*` 已在本函数最上面的域里应答
506
- // 并 `return`,走不到这里;两者都缺席时(local 后端的真实形态——两个工厂它都不实现)本臂如实
507
- // 回答「这个部署没有 sandbox-image-pool」,那对 bakes 路径也是真话。
508
376
  sendError(res, 501, "capability.image_index_required", "the sandbox-image-pool requires a SQL store backend (DB_BACKEND=mysql|pg) — the local backend wires no image index");
509
377
  return;
510
378
  }
511
379
  miss.fell = true;
512
380
  }
513
- /** The image_bake SSE (P2.2/P2.8) — the SAME pump, with the run-trace-SSE-shaped bake frame
514
- * (`data: {kind, seq, ts, ...kindFields}`) + a synthetic `done{failed}` stale frame so a reader settles
515
- * (never hangs) when the runner is lost before the reaper's terminal event lands. */
516
381
  async function streamBakeEvents(req, res, bakes, bakeId, staleMs) {
517
382
  await streamSseLog(req, res, {
518
383
  statusOf: async (id) => {
@@ -521,8 +386,6 @@ async function streamBakeEvents(req, res, bakes, bakeId, staleMs) {
521
386
  },
522
387
  getEvents: (id, after) => bakes.getEvents(id, after),
523
388
  retainedFrom: (id) => bakes.retainedFrom(id),
524
- // The bake frame carries kind+seq+ts INSIDE data (center locks its relay to this), distinct from the
525
- // task_run `{type, ...data}` shape. `level` rides along so an opt-in `raw` log frame is distinguishable.
526
389
  formatEvent: (ev) => ({
527
390
  id: ev.seq,
528
391
  event: ev.kind,
@@ -531,9 +394,6 @@ async function streamBakeEvents(req, res, bakes, bakeId, staleMs) {
531
394
  staleFrame: () => ({ event: "done", data: { kind: "done", status: "FAILED", error: "bake-runner lost (no heartbeat)", digest: null, indexId: null, repo: null, ref: null } }),
532
395
  }, bakeId, staleMs);
533
396
  }
534
- /** The operator poll view of a bake (§P2.1 `{bake:}` envelope, null-not-omitted). Projects the durable row MINUS
535
- * the runner-internal secret/lease fields (`ingestSecret`/`runnerId`/`leaseUntil`/`cancelRequested`/`argv`) — an
536
- * ops surface, but the per-bake ingest secret is a credential and the argv is build-host detail. */
537
397
  function bakeView(b) {
538
398
  return {
539
399
  bakeId: b.bakeId,
@@ -559,9 +419,6 @@ function bakeView(b) {
559
419
  updatedAt: b.updatedAt,
560
420
  };
561
421
  }
562
- /** The RUNNER claim response (§P2.7/§P2.12): the VETTED argv + the per-bake ingest secret (minted at claim, echoed
563
- * on every ingest) + the profile/bands the runner needs for its profile-aware hard deadline. This is the ONLY
564
- * place the ingest secret leaves image-api — to the runner that just won the lease, over the internal claim. */
565
422
  function claimResponse(claimed) {
566
423
  return {
567
424
  bakeId: claimed.bakeId,
@@ -575,143 +432,76 @@ function claimResponse(claimed) {
575
432
  leaseUntil: claimed.leaseUntil,
576
433
  };
577
434
  }
578
- /** Map ONE build.sh JSON line (runner-forwarded, with `lineOrd`) → the durable bake event log + the coarse status
579
- * transition, and on a `done:COMPLETE` trigger the status-monotonic auto-register (§P2.3/§P2.10).
580
- *
581
- * build.sh emits `resolved | state | step | image | manifest | done` (read, not guessed). The runner forwards each
582
- * verbatim plus a per-line `lineOrd` (at-least-once dedupe, uq_line). `done`/`manifest` carry a build-host FS path, not
583
- * the body, so the runner reads the manifest body off the host and forwards it as `manifestBody` on the `done`.
584
- *
585
- * The COARSE status (the reader's terminality column) is driven ONLY by `state`/`done` (§P2.3 MUST-FIX): every
586
- * non-terminal state → `running`; COMPLETE→done, FAILED/CANCELLED→failed. A `state` line that is non-terminal only
587
- * denormalizes the UI progress bar (never the coarse status). The terminal `done` is appended BEFORE the coarse
588
- * row flips (event-before-row latch) so the SSE reader always observes it.
589
- */
590
435
  async function ingestBakeLine(bakes, idx, bake, line, registry) {
591
436
  const bakeId = bake.bakeId;
592
437
  const event = typeof line.event === "string" ? line.event : "";
593
438
  const lineOrd = typeof line.lineOrd === "number" ? line.lineOrd : typeof line.line_ord === "number" ? line.line_ord : null;
594
- // A non-`done` build.sh line: append it as the matching kind + denormalize `state` for the UI progress bar.
595
- // (`resolved | state | step | image | manifest | log` — `manifest` is folded; the body rides the `done`.)
596
439
  if (event !== "done") {
597
440
  if (event === "state") {
598
441
  const state = typeof line.state === "string" ? line.state : null;
599
442
  if (state)
600
- await bakes.setState(bakeId, state); // denormalize only — NEVER the coarse terminality (§P2.3)
443
+ await bakes.setState(bakeId, state);
601
444
  const seq = await bakes.appendEvent(bakeId, "state", { state }, { lineOrd });
602
445
  return { status: 200, body: { accepted: true, seq } };
603
446
  }
604
- // resolved | step | image | log | manifest → store under the build.sh event name as the SSE kind (the runner
605
- // already shapes the data fields; manifest is kept as a `step`-adjacent frame for the trace, body on `done`).
606
447
  const kind = event === "manifest" ? "manifest" : event;
607
448
  const level = event === "log" && line.level === "raw" ? "raw" : null;
608
449
  const { event: _e, lineOrd: _lo, line_ord: _li, ...data } = line;
609
- // L3 (defense-in-depth): redactDeep the ingested non-terminal frame data before it's stored/streamed, mirroring
610
- // streamTaskTrace's tool-arg redaction. build.sh keeps raw cred output off stdout (only resolved.mergedEnv —
611
- // frozen server recipes — is unredacted today), but this closes the resolved.mergedEnv tail + any future
612
- // `?logs=1` raw-tail before it's wired. (redactSecrets is non-exhaustive — hardening, not a guarantee.)
613
450
  const seq = await bakes.appendEvent(bakeId, kind, redactDeep(data), { lineOrd, level });
614
451
  return { status: 200, body: { accepted: true, seq } };
615
452
  }
616
- // ── terminal `done` drive the auto-register + the coarse terminal flip (§P2.10).
617
- const doneStatus = typeof line.status === "string" ? line.status : "FAILED"; // COMPLETE | FAILED | CANCELLED
453
+ const doneStatus = typeof line.status === "string" ? line.status : "FAILED";
618
454
  const exitCode = typeof line.exitCode === "number" ? line.exitCode : doneStatus === "COMPLETE" ? 0 : null;
619
455
  const cancelled = doneStatus === "CANCELLED";
620
456
  const errorText = typeof line.error === "string" ? redactSecrets(line.error) : null;
621
457
  const tag = typeof line.tag === "string" ? line.tag : bake.tag;
622
- let digest = typeof line.digest === "string" && line.digest.length > 0 ? line.digest : null; // let:R22 身份折叠可置空
458
+ let digest = typeof line.digest === "string" && line.digest.length > 0 ? line.digest : null;
623
459
  let indexId = bake.indexId;
624
460
  let repo = bake.repo;
625
461
  let ref = bake.ref;
626
462
  let manifestSha = bake.manifestSha;
627
- // The done STATUS/error can be DOWNGRADED to FAILED on a register-after-build failure (the partial-publish
628
- // contract below) — start from build.sh's reported status and mutate iff the register throws.
629
463
  let finalStatus = doneStatus;
630
464
  let finalError = errorText;
631
465
  let finalExit = exitCode;
632
- // M1: a register-after-build downgrade (COMPLETE→FAILED) must OWN the terminal errorCode — a stale COMPLETE-time
633
- // runner code (e.g. none) must not mask the partial-publish failure. Tracked here, consulted at the errorCode pick.
634
466
  let registerDowngraded = false;
635
- // Auto-register (only a COMPLETE with a manifest body + a digest is registerable). EVENT-BEFORE-ROW: we register
636
- // (idx write) and append the `done` SSE event BEFORE the coarse terminal flip, so a `done` is always observable
637
- // even if the terminal write retries.
638
- //
639
- // PARTIAL-PUBLISH CONTRACT (§P2.2/§P2.10): a register-after-build failure (a TiDB blip AFTER build.sh COMPLETE)
640
- // is surfaced as `digest` present + `indexId` null + `status:"FAILED"` — a built-but-unregistered partial, NOT a
641
- // successful COMPLETE and NOT a re-bake trigger (a P3 reconciliation tick re-registers it). So the done status is
642
- // downgraded COMPLETE→FAILED on the catch, with the digest/repo/ref still carried so the artifact is findable.
643
- // M3: the `register` step frames are EVENT-LOG writes that must follow the SAME terminality gate as the done
644
- // event — buffer them here and replay only when THIS call wins the setTerminal CAS (affectedRows>=1). The idx
645
- // writes (registerBuilding/setIndexId) are status-monotonic and idempotent, so they may run before the CAS.
646
467
  const registerSteps = [];
647
468
  if (doneStatus === "COMPLETE" && digest && idx) {
648
469
  const manifestBody = (line.manifestBody ?? line.manifest);
649
- // 🔴 H3 (zombie/reaped runner): re-read the bake row HERE (the passed `bake` may be stale) and ONLY register
650
- // when it is still non-terminal. A reaped/terminal row must never inject a fresh `building` index row.
651
470
  const fresh = await bakes.getBake(bakeId);
652
471
  const nonTerminal = !!fresh && (fresh.status === "queued" || fresh.status === "running");
653
472
  if (manifestBody && typeof manifestBody === "object" && nonTerminal) {
654
- // 🔴 H3/H5: derive repo/profile from the VETTED control-plane bake row (+ the manifest's profile only as a
655
- // fallback for a --bands "custom" bake) — NEVER trust the runner-supplied manifestBody for the server-fixed
656
- // repo identity. H5: prefix the configured registry host (BAKE_IMAGE_REGISTRY — historically SWR, now
657
- // docker.io/claybobby; SWR deprecated) so the bake row's repo/ref/index-id match the P1 seed
658
- // convention (`${registry}/${profile}`); null registry ⇒ the host-less node-local name (documented
659
- // non-pullable). For a no-push bake the digest is the build.sh LOCAL_ID (node-local, NOT selectable —
660
- // registerBuilding writes status:'building', and latestPublished filters status='published').
661
473
  const profile = bake.profile && bake.profile !== "custom" ? bake.profile : (manifestBody.profile && manifestBody.profile !== "custom" ? manifestBody.profile : "custom");
662
474
  repo = registry ? `${registry}/${profile}` : `sema-images/${profile}`;
663
475
  manifestSha = typeof line.manifestSha === "string" ? line.manifestSha : (typeof line.manifest?.sha256 === "string" ? String(line.manifest.sha256) : bake.manifestSha);
664
- // codex R23:身份候选(digest/repo/manifestSha)先验**后写**——node-postgres 的 UTF-8 编码会把
665
- // lone surrogate 折成 U+FFFD,registerBuilding 可能**成功**落一条身份已变形的 index 行,随后
666
- // 帧级 identityInvalid 折叠才拒帧 → index 已被污染且与拒帧后的 null 身份终态互相矛盾。脏身份
667
- // ⇒ 零 index 写(indexId 强制 null),repo/manifestSha 已按本帧生效值赋出,下方折叠必然命中
668
- // 并独占 FAILED/null 身份终态(该臂不是 register 失败,不走 partial-publish 契约/P3 补注册)。
669
476
  if (pgHasUnstorable(digest) || pgHasUnstorable(repo) || pgHasUnstorable(manifestSha)) {
670
477
  indexId = null;
671
478
  registerSteps.push({ step: "register", status: "skipped", reason: "identity fields contained unstorable bytes" });
672
479
  }
673
480
  else {
674
- // Synthesize a `step:"register"` start around the auto-register so a resuming reader sees the register phase
675
- // (§P2.2: the `register` step is image-api-synthesized — build.sh stops at COMPLETE + the manifest line).
676
481
  registerSteps.push({ step: "register", status: "running" });
677
482
  try {
678
483
  const entry = manifestToIndexEntry(manifestBody, { repo, digest, status: "building", manifestSha, sizeBytes: manifestBody.sizeBytes ?? null, signed: false });
679
- // 🔴 H3: FORCE visibility/tenantId from the control plane — a P2 standard-pool bake is ALWAYS a public,
680
- // tenant-less row. NEVER let the runner-supplied manifestBody (owner/visibility) decide the index row's
681
- // tenancy (a zombie runner could otherwise write a `tenant`-visibility row with an attacker-chosen tenant).
682
484
  entry.visibility = "public";
683
485
  entry.tenantId = null;
684
- // STATUS-MONOTONIC register (§P2.10 MUST-FIX): registerBuilding NEVER downgrades a `published` row — a
685
- // late/duplicate `done` for an already-promoted digest is a no-op on the lifecycle, never a regression.
686
486
  indexId = await idx.registerBuilding(entry);
687
487
  ref = `${repo}@${digest}`;
688
- // codex R24:setIndexId 是 non-terminal 门写(见 store 注释)——本请求悬在 registerBuilding 期间
689
- // 另一 done(如 R23 的 dirty 折叠帧)可能已赢 setTerminal;未绑定⇒indexId 不进本帧 terminal 对象
690
- // (本请求随后必输 CAS,不落行不落事件,index 里的孤 building 行不 publish 不可选、由生命周期兜底)。
691
488
  const bound = await bakes.setIndexId(bakeId, indexId);
692
489
  if (!bound)
693
490
  indexId = null;
694
491
  registerSteps.push({ step: "register", status: "ok" });
695
492
  }
696
493
  catch (err) {
697
- // built-but-unregistered partial (a TiDB blip post-build): downgrade the done to FAILED, keep digest+repo
698
- // (+ ref so the artifact is findable), leave indexId null. The P3 reconciliation tick re-registers it.
699
494
  indexId = null;
700
495
  ref = `${repo}@${digest}`;
701
496
  finalStatus = "FAILED";
702
- registerDowngraded = true; // M1: this FAILED is owned by the register failure, not the runner's code.
703
- finalExit = exitCode ?? 0; // build.sh itself succeeded (exit 0); the register is the post-build failure.
497
+ registerDowngraded = true;
498
+ finalExit = exitCode ?? 0;
704
499
  finalError = redactSecrets(`index register failed after a COMPLETE build (built-but-unregistered partial): ${String(err?.message ?? err)}`);
705
500
  registerSteps.push({ step: "register", status: "fail", reason: "index register failed (TiDB?)" });
706
501
  }
707
502
  }
708
503
  }
709
504
  }
710
- // errorCode + coarse status are computed from the FINAL (possibly-downgraded) status, not build.sh's raw one.
711
- // M1 (§P2.14 #2): PREFER a validated runner-supplied `line.errorCode` (the disk-guard's "disk", a future
712
- // build-time "band-conflict" — codes the exit-code mapper cannot reconstruct) over the recompute. A `cancelled`
713
- // terminal always wins (the kill is authoritative); a register-downgraded FAILED owns its own code (the runner's
714
- // COMPLETE-time code is stale), so it falls back to the exit mapper.
715
505
  const suppliedCode = typeof line.errorCode === "string" && RUNNER_ERROR_CODES.has(line.errorCode)
716
506
  ? line.errorCode
717
507
  : undefined;
@@ -720,14 +510,9 @@ async function ingestBakeLine(bakes, idx, bake, line, registry) {
720
510
  : suppliedCode !== undefined && !registerDowngraded
721
511
  ? suppliedCode
722
512
  : errorCodeForExit(finalExit, cancelled);
723
- // M1: a band-conflict terminal carries the offending band ids so the composer UI can red-highlight the chips
724
- // (§P2.14 #2). Only honored when the FINAL code is band-conflict (a downgraded/other terminal carries none).
725
513
  const conflictBands = errorCode === "band-conflict" && Array.isArray(line.conflictBands)
726
514
  ? line.conflictBands.filter((b) => typeof b === "string")
727
515
  : null;
728
- // codex R22:身份位(digest/repo/ref/manifestSha)带不可存字节=帧无效——在**构造前**统一折叠
729
- // (row/SSE done/HTTP 三面同源一份 effective 对象;store 内折叠留作双保险,值恒等)。COMPLETE 折
730
- // FAILED(null 身份的成功=谎),错误面补一句归因。
731
516
  const identityInvalid = (typeof digest === "string" && pgHasUnstorable(digest)) ||
732
517
  (typeof repo === "string" && pgHasUnstorable(repo)) ||
733
518
  (typeof ref === "string" && pgHasUnstorable(ref)) ||
@@ -738,17 +523,11 @@ async function ingestBakeLine(bakes, idx, bake, line, registry) {
738
523
  repo = null;
739
524
  ref = null;
740
525
  manifestSha = null;
741
- indexId = null; // codex R23:null 身份终态不得声称 index 绑定(register 门已跳过全部 index 写)
526
+ indexId = null;
742
527
  finalError = finalError ? `${finalError}; identity fields contained unstorable bytes` : "terminal frame identity fields contained unstorable bytes";
743
528
  }
744
529
  const finalState = (finalStatus === "COMPLETE" ? "COMPLETE" : finalStatus === "CANCELLED" ? "CANCELLED" : "FAILED");
745
530
  const coarse = coarseStatusForState(finalState);
746
- // M3: flip the coarse terminal row FIRST and gate the terminal `done` EVENT (+ the register `step` frames) on
747
- // WINNING that queued/running→terminal CAS. A late/contradictory terminal (cancel-vs-complete, reaper-vs-recovery)
748
- // sees affectedRows===0 and writes NO event, so the SSE log never replays a second, conflicting terminal `done`.
749
- // Invariant: at most one terminal `done` event per bake, always matching the coarse row outcome (§P2.14).
750
- // L2: the flip is idempotent (`WHERE status IN ('queued','running')`), so a transient throw is safe to retry
751
- // rather than leaving the row `running` with a `done` event the reaper would later contradict.
752
531
  let affectedRows = 0;
753
532
  const terminal = {
754
533
  status: coarse === "done" ? "done" : "failed",
@@ -775,13 +554,9 @@ async function ingestBakeLine(bakes, idx, bake, line, registry) {
775
554
  }
776
555
  }
777
556
  if (lastErr !== undefined) {
778
- // L2: exhausted the bounded retry — DON'T 500 (safeIngest swallows it and the row would stay `running` with no
779
- // terminal event the reaper later contradicts). Return a non-terminal ack so the runner re-POSTs the
780
- // (lineOrd-idempotent) done to re-drive the flip.
781
557
  return { status: 200, body: { accepted: true, terminal: false, needsFlip: true } };
782
558
  }
783
559
  if (affectedRows >= 1) {
784
- // We won the transition: replay the buffered register `step` frames, then append the single terminal `done`.
785
560
  for (const s of registerSteps)
786
561
  await bakes.appendEvent(bakeId, "step", s);
787
562
  await bakes.appendEvent(bakeId, "done", {