@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
@@ -1,25 +1,9 @@
1
- /**
2
- * 鉴权桥 verifier(REGISTRY-REBUILD §5 M3 service 半场)——验 registry 铸的用户 access JWT,
3
- * `sub` 直喂既有 x-agent-principal 体系。规范=`@sema-agent/registry-core/api/auth-bridge` R1-R10(零 IO 纯契约,
4
- * 决策函数直接复用);本模块补齐 IO+crypto 半场:JWKS 拉取/缓存、RS256 真验签(node:crypto,零新依赖)。
5
- *
6
- * 信任模型:worker 已信 registry 的 effective config,同一信任根延伸到它的 JWKS——零新配置项
7
- * (issuer/jwksUrl 默认从 SEMA_REGISTRY_URL 派生;split 部署可 env 显式覆盖)。既有 SERVICE_AUTH_TOKEN(S)
8
- * 分支不动:bearer 长得像 JWT(三段 base64url)才走本分支(looksLikeJwt=路由预过滤,非校验)。
9
- *
10
- * R10:本模块任何路径不 log token/签名段/密钥材料——只 log kid + 错误码 + iss。
11
- */
12
1
  import { createPublicKey, verify as cryptoVerify } from "node:crypto";
13
2
  import { AUTH_BRIDGE_ALGS, DEFAULT_CLOCK_SKEW_SEC, DEFAULT_JWKS_REFRESH_MIN_INTERVAL_SEC, DEFAULT_OFFLINE_GRACE_SEC, REGISTRY_JWKS_PATH, RegistryJwks, classifyTimeClaims, jwksWithinOfflineGrace, shouldForceJwksRefresh, toVerifiedIdentity, } from "@sema-agent/registry-core/api/auth-bridge";
14
3
  const DEFAULT_JWKS_TTL_SEC = 15 * 60;
15
- /** JWKS 响应体字节上限(一个配错或超大响应的端点不应让 worker 无界缓冲;正常 JWKS 千字节级)。 */
16
4
  const MAX_JWKS_BYTES = 256 * 1024;
17
- /** 整 token 长度上限(签名段无独立上限,故对整 bearer 统一设限;正常 JWT ~1KiB)。 */
18
5
  const MAX_TOKEN_CHARS = 16 * 1024;
19
- /** 单段解码上限(header/payload)。 */
20
6
  const MAX_SEGMENT_BYTES = 64 * 1024;
21
- /** base64url 字符集严格校验——Node 的 base64url 解码会容忍集外字符(仍解出同一字节),而契约要求"三段
22
- * base64url"形态严格,故解码前每段必须匹配该字符集。 */
23
7
  const B64URL_RE = /^[A-Za-z0-9_-]+$/;
24
8
  function b64urlJson(seg) {
25
9
  if (seg.length > MAX_SEGMENT_BYTES || !B64URL_RE.test(seg))
@@ -33,10 +17,6 @@ function b64urlJson(seg) {
33
17
  }
34
18
  }
35
19
  const stripSlash = (s) => s.replace(/\/+$/, "");
36
- /**
37
- * registry-JWT verifier(单例,进程级 JWKS 缓存)。`verify(bearer)` 全程不 throw——一切异常折为
38
- * AUTH_BRIDGE_ERROR_CODES(HTTP 面由调用方恒答 generic 401,码只进日志/metrics,R9)。
39
- */
40
20
  export function createRegistryJwtVerifier(opts) {
41
21
  const issuer = stripSlash(opts.issuer);
42
22
  const jwksUrl = opts.jwksUrl ?? `${issuer}${REGISTRY_JWKS_PATH}`;
@@ -46,14 +26,12 @@ export function createRegistryJwtVerifier(opts) {
46
26
  const jwksTtlSec = opts.jwksTtlSec ?? DEFAULT_JWKS_TTL_SEC;
47
27
  const doFetch = opts.fetchImpl ?? fetch;
48
28
  const now = opts.nowMs ?? Date.now;
49
- // 进程级缓存(R3/R8 的状态面;shouldForceJwksRefresh/jwksWithinOfflineGrace 消费的快照即它)。
50
29
  const keys = new Map();
51
30
  let lastFetchAttemptMs;
52
31
  let lastFetchSuccessMs;
53
- let inflight; // 并发 verify 合并成一次拉取(大量未知 kid 时不重复拉取)
32
+ let inflight;
54
33
  const fail = (error, kid) => {
55
34
  opts.metrics?.inc("auth_bridge_verify_total", { outcome: error });
56
- // R10:仅 kid+码+iss。kid 来自**调用方可控**的 token header(16K token 上限内可到 ~10KB)——截断防日志放大。
57
35
  opts.logger?.warn?.("auth_bridge_rejected", { error, ...(kid ? { kid: kid.slice(0, 128) } : {}), iss: issuer });
58
36
  return { ok: false, error };
59
37
  };
@@ -66,19 +44,16 @@ export function createRegistryJwtVerifier(opts) {
66
44
  const res = await doFetch(jwksUrl, { redirect: "error", signal: AbortSignal.timeout(10_000) });
67
45
  if (!res.ok)
68
46
  throw new Error(`jwks HTTP ${res.status}`);
69
- // 先看 Content-Length 早拒;chunked/无 CL 的响应会整体读入后按字节拒(非流式截断——jwksUrl 是
70
- // operator 钉死的可信 registry + redirect:"error" + 10s timeout,瞬时内存风险接受,诚实记档)。
71
47
  const cl = Number(res.headers.get("content-length"));
72
48
  if (Number.isFinite(cl) && cl > MAX_JWKS_BYTES)
73
49
  throw new Error("jwks content-length too large");
74
50
  const buf = Buffer.from(await res.arrayBuffer());
75
51
  if (buf.byteLength > MAX_JWKS_BYTES)
76
52
  throw new Error("jwks body too large");
77
- const doc = RegistryJwks.parse(JSON.parse(buf.toString("utf8"))); // zod:形状不对=拉取失败(R8 路),绝不采用半坏文档
53
+ const doc = RegistryJwks.parse(JSON.parse(buf.toString("utf8")));
78
54
  const next = new Map();
79
55
  for (const k of doc.keys) {
80
56
  try {
81
- // 公钥成员 only(契约警告:即便 registry 出 bug 带了私有成员也绝不使用/回显)。
82
57
  next.set(k.kid, createPublicKey({ key: { kty: "RSA", n: k.n, e: k.e }, format: "jwk" }));
83
58
  }
84
59
  catch {
@@ -91,7 +66,6 @@ export function createRegistryJwtVerifier(opts) {
91
66
  lastFetchSuccessMs = now();
92
67
  }
93
68
  catch (e) {
94
- // 失败=保留旧缓存(R8 离线宽限判定在 verify 侧);只记账不 throw。
95
69
  opts.logger?.warn?.("auth_bridge_jwks_fetch_failed", { iss: issuer, error: String(e).slice(0, 200) });
96
70
  }
97
71
  finally {
@@ -101,8 +75,6 @@ export function createRegistryJwtVerifier(opts) {
101
75
  return inflight;
102
76
  }
103
77
  async function verify(bearer) {
104
- // 延迟观测(auth_bridge_verify_seconds):热路径(kid 命中)应为亚毫秒级;冷路径(JWKS 拉取)带一次
105
- // 网络 RTT——首请求/轮换时的尾延迟在这根直方图上可见,不用猜。
106
78
  const t0 = now();
107
79
  try {
108
80
  return await verifyInner(bearer);
@@ -115,57 +87,43 @@ export function createRegistryJwtVerifier(opts) {
115
87
  if (bearer.length > MAX_TOKEN_CHARS)
116
88
  return fail("malformed");
117
89
  const parts = bearer.split(".");
118
- // 三段非空 + 每段 base64url 字符集(b64urlJson 已校验 header/payload;签名段在此单独校验)。
119
90
  if (parts.length !== 3 || parts.some((p) => p.length === 0) || !B64URL_RE.test(parts[2]))
120
91
  return fail("malformed");
121
92
  const header = b64urlJson(parts[0]);
122
93
  const payload = b64urlJson(parts[1]);
123
94
  if (!header || !payload)
124
95
  return fail("malformed");
125
- // R1 — alg 固定在调用侧:只接受 RS256,不读 token 的 alg 字段来决定验法(none/HS*/ES*/PS* 一律拒)。
126
- // 错误码归类跟契约(AUTH_BRIDGE_ERROR_CODES 注释):disallowed alg / 缺 kid = `malformed`
127
- // (invalid_signature 留给"键在、验签不过"——异源复审对齐,跨侧 metrics 口径一致)。
128
96
  if (header.alg !== AUTH_BRIDGE_ALGS[0])
129
97
  return fail("malformed");
130
98
  const kid = typeof header.kid === "string" ? header.kid : undefined;
131
99
  if (!kid)
132
100
  return fail("malformed");
133
- // R4 — issuer 先于取键比较(iss 不对根本不必碰 JWKS)。
134
101
  const iss = typeof payload.iss === "string" ? stripSlash(payload.iss) : undefined;
135
102
  if (iss !== issuer)
136
103
  return fail("unknown_issuer", kid);
137
- // R5 — 时间窗(60s 偏移;exp 必须在)。
138
104
  if (classifyTimeClaims(payload, Math.floor(now() / 1000), clockSkewSec) !== "ok")
139
105
  return fail("expired", kid);
140
- // R2/R3 — 取键。热路径:kid 命中 ⇒ 直用,零 fetch(TTL 陈旧只在无在途拉取且过节流时【后台】刷一次
141
- // [void,不 await],不阻塞验证)。kid miss ⇒ 先 await 任何在途拉取(并发请求不应被 lastFetchAttemptMs
142
- // 节流跳过而错过正在完成的刷新),再查;仍 miss 且过节流 ⇒ 强制刷一次。
143
106
  const snapshot = () => ({ cachedKids: [...keys.keys()], lastFetchAttemptMs, nowMs: now() });
144
107
  if (keys.has(kid)) {
145
108
  const ttlStale = lastFetchSuccessMs === undefined || now() - lastFetchSuccessMs > jwksTtlSec * 1000;
146
109
  if (ttlStale && !inflight && (lastFetchAttemptMs === undefined || now() - lastFetchAttemptMs >= refreshMin * 1000)) {
147
- void refreshJwks(); // 后台刷新,当前请求继续用现有 key
110
+ void refreshJwks();
148
111
  }
149
112
  }
150
113
  else {
151
114
  if (inflight)
152
- await inflight; // 有在途拉取:等它完成再查(可能正好带来这个 kid)
115
+ await inflight;
153
116
  if (!keys.has(kid) && shouldForceJwksRefresh(kid, snapshot(), { jwksRefreshMinIntervalSec: refreshMin }))
154
117
  await refreshJwks();
155
118
  }
156
119
  const key = keys.get(kid);
157
120
  if (!key) {
158
- // R8 — kid 仍未命中:有【活】缓存(有成功拉取且在 24h 宽限内)⇒ 键该在却不在=token 与当前键集不符
159
- // (invalid_signature)。从没成功拉过 / 超宽限 ⇒ jwks_unavailable(失败即拒:不用 `keys.size>0` 兜底,
160
- // 因为超宽限的陈旧缓存 size>0 时也不应把 unavailable 归成 invalid_signature)。
161
121
  const graceOk = lastFetchSuccessMs !== undefined && jwksWithinOfflineGrace({ lastFetchSuccessMs, nowMs: now() }, { offlineGraceSec });
162
122
  return graceOk ? fail("invalid_signature", kid) : fail("jwks_unavailable", kid);
163
123
  }
164
124
  if (lastFetchSuccessMs !== undefined && !jwksWithinOfflineGrace({ lastFetchSuccessMs, nowMs: now() }, { offlineGraceSec })) {
165
- // 缓存超出 24h 离线宽限——即便 kid 命中也拒(R8:宽限后不再使用陈旧键)。
166
125
  return fail("jwks_unavailable", kid);
167
126
  }
168
- // 真验签(RS256 = RSASSA-PKCS1-v1_5 + SHA-256 over `header.payload`)。
169
127
  let sigOk = false;
170
128
  try {
171
129
  sigOk = cryptoVerify("RSA-SHA256", Buffer.from(`${parts[0]}.${parts[1]}`, "utf8"), key, Buffer.from(parts[2], "base64url"));
@@ -175,7 +133,6 @@ export function createRegistryJwtVerifier(opts) {
175
133
  }
176
134
  if (!sigOk)
177
135
  return fail("invalid_signature", kid);
178
- // R6/R7/R9 — 身份映射(契约纯函数)。
179
136
  const mapped = toVerifiedIdentity(payload, opts.audience ? { audience: opts.audience } : undefined);
180
137
  if (!mapped.ok)
181
138
  return fail(mapped.error, kid);
package/dist/auth-keys.js CHANGED
@@ -1,15 +1,3 @@
1
- /**
2
- * AUTH KEY-SET parsing (design/158 A4) — the env→key-set decoders for the two D-G credential planes, moved VERBATIM
3
- * out of security.ts: {@link parseApprovalHmacKeys} (APPROVAL_HMAC_KEYS — the rotating HMAC secret set, decision
4
- * INTEGRITY) and {@link parsePrincipalJwks} (PRINCIPAL_JWT_PUBKEYS — the rotating issuer public-key set, principal
5
- * IDENTITY). config.ts consumes ONLY these two functions + their row types at env-load time, and importing them from
6
- * security.ts made the base config layer value-depend on the whole 55KiB auth module (lens2 §F). This file is a pure
7
- * leaf (JSON.parse + shape filters, zero repo imports); the VERIFIERS that consume the parsed key-sets
8
- * (verifyApprovalHmac / verifyPrincipalJwt) stay in ./approval-hmac.ts and ./principal-jwt.ts; consumers import these four names from HERE ([2354] 兼容面全清).
9
- */
10
- /** Parse APPROVAL_HMAC_KEYS — a sema-registry-managed, rotating key-SET, as a JSON array of {kid,key,status}.
11
- * Empty / unset / unparseable ⇒ `[]` ⇒ HMAC verification is OFF (D-G inactive; back-compat, BFF-only door).
12
- * fail-closed 语义与改前**逐字相同**;新增的只有 `onReject`(#210:每一条被丢弃的条目都要说出来)。 */
13
1
  export function parseApprovalHmacKeys(raw, onReject) {
14
2
  if (!raw)
15
3
  return [];
@@ -18,9 +6,6 @@ export function parseApprovalHmacKeys(raw, onReject) {
18
6
  v = JSON.parse(raw);
19
7
  }
20
8
  catch {
21
- // ⚠️ 解析器自带的 message **不能**转述:V8 的 `JSON.parse` 报错里嵌了输入片段
22
- // (`Unexpected token 'T', "TOP_SECRET"... is not valid JSON`),而这条 env 的内容就是对称密钥 ——
23
- // 把它写进 boot 日志等于把密钥前缀印出来。只说「不是合法 JSON」,定位靠 env 名本身。
24
9
  onReject({ index: "-", reason: "not valid JSON (value withheld: this env holds secrets)" });
25
10
  return [];
26
11
  }
@@ -43,10 +28,6 @@ export function parseApprovalHmacKeys(raw, onReject) {
43
28
  });
44
29
  return out;
45
30
  }
46
- /** Parse PRINCIPAL_JWT_PUBKEYS — a JSON array of {kid,alg,key,status}. Empty/unset/malformed ⇒ [] ⇒ no trust
47
- * anchor ⇒ the direct door cannot open (D-G inactive). Each entry must have kid + a pinned alg + a PEM key.
48
- * #210: fail-closed 语义逐字不变,但每条被丢弃的条目经 `onReject` 报出(序号 + 缺陷字段名,不带值)——
49
- * 这里的 key 是**公**钥,但 reason 仍只说字段名,与 HMAC 那条同形(两个出口的口径不该分岔)。 */
50
31
  export function parsePrincipalJwks(raw, onReject) {
51
32
  if (!raw)
52
33
  return [];
@@ -56,8 +37,6 @@ export function parsePrincipalJwks(raw, onReject) {
56
37
  v = JSON.parse(raw);
57
38
  }
58
39
  catch {
59
- // 同 parseApprovalHmacKeys:不转述 V8 的 message(它嵌输入片段)。这一口装的是**公**钥,但两个出口
60
- // 的口径不该分岔 —— 一个「视情况才脱敏」的规矩,迟早会在错的那一侧被抄走。
61
40
  onReject({ index: "-", reason: "not valid JSON (value withheld)" });
62
41
  return [];
63
42
  }
@@ -73,8 +52,6 @@ export function parsePrincipalJwks(raw, onReject) {
73
52
  return;
74
53
  }
75
54
  const bad = ["kid", "key"].filter((f) => typeof e[f] !== "string");
76
- // alg 单列:缺席与「不在闭集」是两种手滑,但都必须点名 `alg` —— 运维看到字段名才知道去改哪一格
77
- // (alg-confusion 防线就挂在这条钉上,静默滤掉等于防线自己消失了没人知道)。
78
55
  if (typeof e.alg !== "string")
79
56
  bad.push("alg");
80
57
  else if (!ALGS.has(e.alg))
@@ -1,12 +1,3 @@
1
- /**
2
- * bake-runner ENTRYPOINT (IMAGE-API-DESIGN.md §P2.0/§P2.12) — the privileged build-host daemon. It wires the REAL I/O
3
- * (HTTP to image-api, the setsid-detached build.sh child, the fs manifest read, git checkout, df) into the
4
- * I/O-injected `BakeRunner` core (./runner.ts). This process runs ONLY on the build-host build host — it CANNOT be
5
- * unit-run here (no docker/ctr/recipes); the testable logic is the pure protocol + the runner orchestration.
6
- *
7
- * Crown-jewel discipline (§P2.4e): the only secrets are BAKE_RUNNER_TOKEN (the operator-principal bearer for
8
- * claim/heartbeat) and the per-bake ingest secret minted by image-api at claim — neither is ever logged.
9
- */
10
1
  import { spawn, exec as execCb } from "node:child_process";
11
2
  import { readFile } from "node:fs/promises";
12
3
  import { promisify } from "node:util";
@@ -15,12 +6,6 @@ import { parseNumOrFailNonNegative } from "../config.js";
15
6
  import { shellQuote as shellSafe } from "../plugins/remote-shell.js";
16
7
  import { BakeRunner, } from "./runner.js";
17
8
  const exec = promisify(execCb);
18
- // 鲁棒性批5 A3(2026-08-05):claim/ingest/heartbeat 三个 fetch 此前零超时——bake-runner 的整条 loop()
19
- // 是这台构建宿主机唯一的主循环,一次悬挂的 image-api 连接(网络分区/黑洞 TCP)会让 claim/heartbeat 永久
20
- // 挂起,直到底层 socket 超时(可能几十分钟),期间既不上报心跳也不能认领新 bake——整机静默停摆。
21
- // 上界两档:claim/ingest 不在心跳热路径,给 30s 吃满慢网 RTT + 大 body;heartbeat 必须显著小于
22
- // BAKE_HEARTBEAT_MS 的默认量级(30s)——否则一次超时本身就吃掉一整个心跳周期,取 10s(同
23
- // fleet-client.ts `FLEET_CLIENT_FETCH_TIMEOUT_MS` 判据的普适形)。
24
9
  const BAKE_CLAIM_INGEST_FETCH_TIMEOUT_MS = 30_000;
25
10
  const BAKE_HEARTBEAT_FETCH_TIMEOUT_MS = 10_000;
26
11
  function loadEnv() {
@@ -38,26 +23,14 @@ function loadEnv() {
38
23
  recipeDir: process.env.RECIPE_DIR || "/opt/recipes",
39
24
  buildShPath: process.env.BUILD_SH_PATH || "e2b-template/dev-sandbox/build.sh",
40
25
  dataDir: process.env.BAKE_DATA_DIR || "/data",
41
- // A2/E5 (gap-sweep 2026-08-01): these were bare `Number(process.env.X || default)` — a unit-suffixed or
42
- // typo'd value (`"30s"`) silently became NaN, and NaN survives every downstream `??`/`?? default` guard
43
- // (those only catch null/undefined). The fallout is a SILENT-LOOSENING pair: `clock.sleep(NaN)` resolves
44
- // ~immediately (`setTimeout(fn, NaN)` behaves like a 0ms timer) turning the lease heartbeat into a tight
45
- // POST loop against image-api, and `freeGb < NaN` is always false so the pre-flight disk guard silently
46
- // never fires. `parseNumOrFailNonNegative` (config.ts, shared with leader/wire.ts's four resource knobs)
47
- // fails loud at bake-runner startup instead — non-numeric OR negative both throw; unset/empty still falls
48
- // back to the documented default (zero behavior change on the happy path).
49
26
  heartbeatMs: parseNumOrFailNonNegative("BAKE_HEARTBEAT_MS", process.env.BAKE_HEARTBEAT_MS || "30000"),
50
27
  idlePollMs: parseNumOrFailNonNegative("BAKE_IDLE_POLL_MS", process.env.BAKE_IDLE_POLL_MS || "5000"),
51
28
  minFreeGb: parseNumOrFailNonNegative("IMAGE_BAKE_MIN_FREE_GB", process.env.IMAGE_BAKE_MIN_FREE_GB || "20"),
52
29
  };
53
30
  }
54
- /** The real image-api HTTP client. Authenticates claim/heartbeat with the runner bearer; ingest additionally
55
- * carries the per-bake ingest secret header (so image-api can reject a stale/rogue runner — §P2.4e). */
56
31
  function makeApiClient(env, log) {
57
32
  const auth = { authorization: `Bearer ${env.token}`, "content-type": "application/json" };
58
33
  return {
59
- // Long-poll claim (§P2.12): the bare `POST …/bakes/claim` finds the oldest queued bake AND leases it in one
60
- // call (the discovery + the single-flight CAS server-side). 204 ⇒ the queue is empty (the runner idle-polls).
61
34
  async claimNext() {
62
35
  const res = await fetch(`${env.imageApiBase}/v1/images/bakes/claim`, {
63
36
  method: "POST",
@@ -66,23 +39,13 @@ function makeApiClient(env, log) {
66
39
  signal: AbortSignal.timeout(BAKE_CLAIM_INGEST_FETCH_TIMEOUT_MS),
67
40
  });
68
41
  if (res.status === 204)
69
- return null; // empty queue — the routine "nothing to do" case, no log line
70
- // C6/C1 (2026-08-01): 401 (bad runner credential) / 409 (lost the single-flight CAS to another runner
71
- // replica) / 5xx previously fell into this SAME silent-`null` branch as an empty queue — a misconfigured
72
- // BAKE_RUNNER_TOKEN and a healthy idle runner were indistinguishable from the outside (both just idle-poll
73
- // forever with zero log lines). `bake_claim_error` (loop()'s catch) only fires on a THROWN error — a non-ok
74
- // HTTP response never throws — so this warn is the only trace a non-2xx claim response leaves anywhere.
42
+ return null;
75
43
  if (!res.ok) {
76
44
  log.warn("bake_claim_rejected", { status: res.status });
77
45
  return null;
78
46
  }
79
47
  const c = (await res.json());
80
48
  if (!c.bakeId || !Array.isArray(c.argv) || !c.ingestSecret) {
81
- // A malformed 2xx body is worse than a rejection: image-api's claim call ATOMICALLY leases the bake
82
- // server-side before returning it, so a shape we refuse to trust here is a bake already checked OUT
83
- // from the queue that we are about to drop on the floor. We still can't safely act on an untrusted
84
- // shape (no bakeId ⇒ no way to release/fail it), so this warn is the only surfacing point until
85
- // image-api's stale-lease reaper reclaims it (§P2.7) — see runner.ts module doc for the reaper backstop.
86
49
  log.warn("bake_claim_malformed", { status: res.status, bakeId: typeof c.bakeId === "string" ? c.bakeId : undefined });
87
50
  return null;
88
51
  }
@@ -108,11 +71,10 @@ function makeApiClient(env, log) {
108
71
  return {
109
72
  accepted: res.ok && body.accepted !== false,
110
73
  cancelRequested: body.cancelRequested === true,
111
- leaseValid: res.status !== 409, // a 409 on ingest = the lease/claim no longer ours
74
+ leaseValid: res.status !== 409,
112
75
  };
113
76
  },
114
77
  async heartbeat(bakeId, ingestSecret) {
115
- // A heartbeat is a no-frame ingest: image-api advances the lease and echoes the cancel flag (§P2.7/§P2.11).
116
78
  const res = await fetch(`${env.imageApiBase}/v1/images/bakes/${encodeURIComponent(bakeId)}/ingest`, {
117
79
  method: "POST",
118
80
  headers: { ...auth, "x-bake-ingest-secret": ingestSecret },
@@ -128,13 +90,9 @@ function makeApiClient(env, log) {
128
90
  },
129
91
  };
130
92
  }
131
- /** The real child spawner: `setsid`-style detached process GROUP so a cancel/deadline kill signals the whole tree
132
- * (channel-close / a plain kill of the immediate child does NOT stop a detached build — §P2.11). */
133
93
  function makeSpawner() {
134
94
  return {
135
95
  spawn(opts) {
136
- // detached:true → the child becomes a process-group leader (its pgid == its pid); `process.kill(-pid, sig)`
137
- // then signals the WHOLE group. We DON'T `unref()` — the runner waits on the exit promise.
138
96
  const child = spawn(opts.argv[0], opts.argv.slice(1), {
139
97
  cwd: opts.cwd,
140
98
  detached: true,
@@ -146,13 +104,12 @@ function makeSpawner() {
146
104
  bufferLines(child.stderr, opts.onStderrLine);
147
105
  const exited = new Promise((resolve) => {
148
106
  child.on("exit", (code, signal) => resolve({ code, signal }));
149
- child.on("error", () => resolve({ code: 127, signal: null })); // spawn failed (build.sh missing, etc.)
107
+ child.on("error", () => resolve({ code: 127, signal: null }));
150
108
  });
151
109
  const handle = {
152
110
  pgid,
153
111
  exited,
154
112
  killGroup(signal) {
155
- // Negative pid → signal the process GROUP (the detached tree), not just the immediate child.
156
113
  if (pgid > 0)
157
114
  process.kill(-pgid, signal);
158
115
  },
@@ -161,8 +118,6 @@ function makeSpawner() {
161
118
  },
162
119
  };
163
120
  }
164
- /** Split a readable stream into lines, invoking `onLine` per complete line (the runner parses ONLY stdout as JSON;
165
- * stderr lines are the failure tail). Carries a partial trailing line across chunks; flushes it on stream end. */
166
121
  function bufferLines(stream, onLine) {
167
122
  if (!stream)
168
123
  return;
@@ -182,7 +137,6 @@ function bufferLines(stream, onLine) {
182
137
  onLine(buf);
183
138
  });
184
139
  }
185
- /** The real host ops: fs manifest read, df /data, recipe checkout, and the thin docker-push big-blob retry. */
186
140
  function makeHostOps(env) {
187
141
  return {
188
142
  async readManifest(path) {
@@ -193,14 +147,6 @@ function makeHostOps(env) {
193
147
  return stdout;
194
148
  },
195
149
  async checkoutRecipe(ref) {
196
- // The recipe dir is a git checkout of the service repo pinned to the runner's deployed commit (NOT a frozen
197
- // copy) so build.sh's `git rev-parse` gitsha is REAL and the manifest's recipeGitSha is replay-sufficient.
198
- // A fixed ref (the deployed image's commit) → fetch + checkout is idempotent; a non-default ref is fetched.
199
- // 🔴 A ref may be a branch/tag (fetchable by name) OR a BARE COMMIT SHA — and `git fetch origin <sha>` fails
200
- // on most servers ("couldn't find remote ref <sha>"; only refs are fetchable). So try the cheap shallow
201
- // ref-fetch, and on failure fall back to fetching the repo and checking the SHA out of the local history
202
- // (the init-container clone usually already contains it). RECIPE_REF=HEAD skips this entirely (uses the
203
- // checkout as-is — the prod path, the init-container pins the commit). [live-caught on center's prod deploy]
204
150
  if (ref && ref !== "HEAD") {
205
151
  const dir = shellSafe(env.recipeDir);
206
152
  try {
@@ -211,14 +157,9 @@ function makeHostOps(env) {
211
157
  await exec(`git -C ${dir} fetch origin && git -C ${dir} checkout --quiet --detach ${shellSafe(ref)}`);
212
158
  }
213
159
  }
214
- // build.sh resolves itself relative to its own dir; the cwd for the spawn is the recipe checkout root so the
215
- // path RECIPE_DIR + BUILD_SH_PATH is the script the runner invokes.
216
160
  return env.recipeDir;
217
161
  },
218
162
  async dockerPushRetry(tag, attempts) {
219
- // Thin `docker push` wrapper (NOT build.sh --replay — that re-bakes + hits the dup-check, exit 10). Retries a
220
- // flaky big-blob registry layer push with backoff (born on SWR — deprecated; pool now docker.io/claybobby);
221
- // NO_PROXY (incl. the registry domain) lives in this process's env.
222
163
  for (let i = 0; i < attempts; i++) {
223
164
  try {
224
165
  await exec(`docker push ${shellSafe(tag)}`);
@@ -251,7 +192,7 @@ async function main() {
251
192
  recipeRef: env.recipeRef,
252
193
  heartbeatMs: env.heartbeatMs,
253
194
  minFreeGb: env.minFreeGb,
254
- }); // NB: the token + per-bake ingest secret are NEVER logged (§P2.4e)
195
+ });
255
196
  const runner = new BakeRunner({
256
197
  api: makeApiClient(env, log),
257
198
  spawner: makeSpawner(),
@@ -272,11 +213,9 @@ async function main() {
272
213
  process.on("SIGINT", () => shutdown("SIGINT"));
273
214
  await runner.loop();
274
215
  }
275
- // Only run when invoked directly (not when imported by a test).
276
216
  const invokedDirectly = process.argv[1] !== undefined && /bake-runner[/\\]main\.(ts|js)$/.test(process.argv[1]);
277
217
  if (invokedDirectly) {
278
218
  main().catch((err) => {
279
- // eslint-disable-next-line no-console
280
219
  console.error(JSON.stringify({ ts: new Date().toISOString(), level: "error", msg: "bake_runner_fatal", err: String(err) }));
281
220
  process.exit(1);
282
221
  });
@@ -1,24 +1,7 @@
1
- /**
2
- * bake-runner PURE protocol layer (IMAGE-API-DESIGN.md §P2.12) — the line→frame mapping + the small policy
3
- * helpers, factored free of I/O so they are unit-testable WITHOUT the privileged build-host build host (the runner
4
- * itself can only be exercised live; this is the part a unit test pins).
5
- *
6
- * The runner spawns `build.sh <vetted argv> --json` and forwards each STDOUT line to image-api's ingest. build.sh
7
- * emits a small set of JSON object lines (`resolved | state | step | image | manifest | done`) on stdout and tees
8
- * the raw `docker build` output to `$BUILD_DIR/build.log` (NOT onto stdout). On failure it does `tail -40 build.log
9
- * >&2` — that is STDERR, the failure tail, NOT JSON; we parse JSON ONLY from stdout (a naive JSON.parse over both
10
- * crashes the parser — §P2.5). Each forwarded frame carries an incrementing `lineOrd` (at-least-once ingest dedupe;
11
- * image-api's `uq_line` ignores a re-sent ordinal — §P2.10).
12
- */
13
- /**
14
- * Parse ONE raw stdout line into a build.sh event, or null when it is not a JSON object line (a blank line, a
15
- * stray non-JSON print). build.sh's protocol is strictly one JSON OBJECT per stdout line (`jq -nc`), so anything
16
- * that is not a `{…}` object is treated as non-protocol noise the caller forwards as a `log` frame (NOT crashed on).
17
- */
18
1
  export function parseBuildShLine(raw) {
19
2
  const s = raw.trim();
20
3
  if (s.length === 0 || s[0] !== "{")
21
- return null; // fast reject: protocol lines are JSON objects
4
+ return null;
22
5
  let parsed;
23
6
  try {
24
7
  parsed = JSON.parse(s);
@@ -30,49 +13,26 @@ export function parseBuildShLine(raw) {
30
13
  return null;
31
14
  const obj = parsed;
32
15
  if (typeof obj.event !== "string")
33
- return null; // not a protocol line → caller forwards as a log frame
16
+ return null;
34
17
  return obj;
35
18
  }
36
- /**
37
- * Map ONE stdout line → the ingest frame the runner POSTs (the PURE core a unit test pins). A parseable protocol
38
- * line is forwarded under its own `event`; an unparseable stdout line becomes an `info` log frame (build.sh's
39
- * structured stdout is naturally quiet, so this is rare — usually a generator print). The raw `docker build` tail
40
- * never reaches here: it is on stderr (the runner only feeds stdout through this) — §P2.5.
41
- */
42
19
  export function lineToFrame(raw, lineOrd) {
43
20
  const ev = parseBuildShLine(raw);
44
21
  if (ev === null) {
45
- // Non-protocol stdout noise → an info log frame (NOT crashed on, NOT a raw docker tail — that is stderr).
46
22
  return { event: "log", lineOrd, level: "info", line: raw.replace(/\s+$/, "") };
47
23
  }
48
24
  const { event, ...rest } = ev;
49
25
  return { event, lineOrd, ...rest };
50
26
  }
51
- /**
52
- * Synthesize the `docker-build status:running elapsedSec` heartbeat step (§P2.2) — emitted every ~30s during the
53
- * long, silent `docker build` (with logs off) so a resuming reader sees forward motion instead of a dead spinner.
54
- * It is a synthesized frame, so it still carries a `lineOrd` (image-api dedupes the same way).
55
- */
56
27
  export function dockerBuildTickFrame(lineOrd, elapsedSec) {
57
28
  return { event: "step", lineOrd, step: "docker-build", status: "running", elapsedSec };
58
29
  }
59
- /**
60
- * Synthesize the terminal `done` the runner sends when build.sh did NOT emit one (a hard cancel/timeout kill —
61
- * build.sh has no SIGTERM trap, so a killed process never prints `done:CANCELLED`/`done:FAILED`; the runner MUST
62
- * synthesize it — §P2.11). `cancelled` → `CANCELLED`; a timeout → `FAILED exitCode:124`.
63
- */
64
30
  export function synthDoneFrame(lineOrd, kind, detail) {
65
31
  if (kind === "cancelled") {
66
32
  return { event: "done", lineOrd, status: "CANCELLED", exitCode: null, error: detail?.error ?? "cancelled by operator" };
67
33
  }
68
34
  return { event: "done", lineOrd, status: "FAILED", exitCode: 124, error: detail?.error ?? "bake hard-deadline exceeded (killed)" };
69
35
  }
70
- /**
71
- * The runner-side terminal `done` for a build.sh CHILD that exited non-zero WITHOUT printing its own `done` (build.sh
72
- * normally dies via `die()` which DOES print a `done:FAILED`, but a SIGKILL or a crash before the trap can leave the
73
- * child exiting non-zero silently). The runner backstops with a FAILED done carrying the child exit code + the
74
- * captured stderr failure tail (the `tail -40 build.log` build.sh prints on failure), redacted by image-api on store.
75
- */
76
36
  export function exitDoneFrame(lineOrd, exitCode, stderrTail) {
77
37
  const tail = stderrTail.trim();
78
38
  return {
@@ -83,16 +43,10 @@ export function exitDoneFrame(lineOrd, exitCode, stderrTail) {
83
43
  error: tail.length > 0 ? `build.sh exited ${exitCode}: ${lastLines(tail, 12)}` : `build.sh exited ${exitCode}`,
84
44
  };
85
45
  }
86
- /** Keep only the last `n` lines of a (possibly long) stderr tail — bounds the error string we forward. */
87
46
  export function lastLines(text, n) {
88
47
  const lines = text.split(/\r?\n/).filter((l) => l.length > 0);
89
48
  return lines.slice(Math.max(0, lines.length - n)).join("\n");
90
49
  }
91
- /**
92
- * The profile-aware HARD DEADLINE (§P2.7/§P2.11) the runner enforces by killing the process group. flutter/full are
93
- * the heavy profiles (~60min); everything else ~45min. A bands build ("custom") is treated as heavy if it includes
94
- * a heavy band. The reaper backstops this; the runner is the primary enforcer (it owns the child).
95
- */
96
50
  export function deadlineMsForProfile(profile, bands) {
97
51
  const HEAVY_MS = 60 * 60 * 1000;
98
52
  const NORMAL_MS = 45 * 60 * 1000;
@@ -104,25 +58,19 @@ export function deadlineMsForProfile(profile, bands) {
104
58
  return HEAVY_MS;
105
59
  return NORMAL_MS;
106
60
  }
107
- /** Parse `df -P -BG /data` (POSIX, 1G blocks) → free GiB on the build-host data fs. Returns null if unparseable
108
- * (the caller then fails OPEN — a df parse miss must not block a bake; the reaper still guards a mid-build fill). */
109
61
  export function parseDfFreeGb(dfOutput) {
110
- // `df -P -BG <path>` prints a header line then one data line; the "Available" column is field index 3 (0-based),
111
- // a value like "812G". POSIX `-P` guarantees one physical line per fs (no wrapping), so field positions are fixed.
112
62
  const lines = dfOutput.trim().split(/\r?\n/);
113
63
  if (lines.length < 2)
114
64
  return null;
115
65
  const data = lines[lines.length - 1].trim().split(/\s+/);
116
66
  if (data.length < 4)
117
67
  return null;
118
- const avail = data[3]; // Filesystem 1G-blocks Used Available …
68
+ const avail = data[3];
119
69
  const m = /^(\d+)G?$/.exec(avail);
120
70
  if (!m)
121
71
  return null;
122
72
  return Number(m[1]);
123
73
  }
124
- /** The path build.sh's `done`/`manifest` line points at (a build-host FS path). The done carries `manifest:<path>`, the
125
- * manifest event carries `path` — the runner reads whichever is present off disk for the auto-register body. */
126
74
  export function manifestPathFromLine(doneLine, lastManifestPath) {
127
75
  if (typeof doneLine.manifest === "string" && doneLine.manifest.length > 0)
128
76
  return doneLine.manifest;
@@ -130,9 +78,6 @@ export function manifestPathFromLine(doneLine, lastManifestPath) {
130
78
  return doneLine.path;
131
79
  return lastManifestPath;
132
80
  }
133
- /** Pull the post-push runtime digest the auto-register should key on. build.sh rewrites DIGEST from RepoDigests[0]
134
- * AFTER a push, so the `done`/`image` line's `digest` is already the post-push registry RepoDigest on a push bake and the
135
- * LOCAL_ID on a no-push bake — the runner forwards it verbatim; this just normalizes the field name (§P2.10). */
136
81
  export function digestFromDoneLine(doneLine, lastImageDigest) {
137
82
  if (typeof doneLine.digest === "string" && doneLine.digest.length > 0)
138
83
  return doneLine.digest;