@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,55 +1,7 @@
1
- /**
2
- * Sandbox-image-pool INDEX store (IMAGE-API-DESIGN.md §1, pool design §8) — SINGLE-FILE DUAL-DIALECT
3
- * (design/158 A12 定型半场). ONE implementation, TWO dialects; the historical `TiDBImageIndex` /
4
- * `PgImageIndex` class names survive as thin ctor subclasses so every consumer (store-backend.ts,
5
- * pg-pool.ts, the unit + real-DB suites) is untouched.
6
- *
7
- * One row per published/building image DIGEST — the immutable runtime ref (design §6: refs are @sha256:...).
8
- * A queryable projection of build.sh's replay-sufficient `sandbox-manifest.json` + the publish lifecycle
9
- * `status` (building → published → deprecated / failed). This is the SINGLE SOURCE OF TRUTH for "what can a
10
- * sandbox run"; `POST /v1/images/select` resolves a profile → digest off it (the §7 per-sandbox hot path).
11
- *
12
- * Visibility is enforced IN THE QUERY (Codex A1, mirrors the assistant per-user scoping): a tenant principal
13
- * sees `public` rows + its OWN `tenant` rows only; an operator sees all. mapRow/ImageRow are the row-mapping
14
- * surface shared by BOTH dialects — they live in the backend-neutral store-contracts.ts (design/158 A3 沉底).
15
- *
16
- * ── Dialect deltas, kept EXPLICIT ────────────────────────────────────────────────────────────────────
17
- * - `?` placeholders vs positional `$n` (and the placeholder-arity fallout in `list`/`setStatus`, which
18
- * track a running `$n` index; TiDB's `?` needs no such bookkeeping).
19
- * - upsert form: `ON DUPLICATE KEY UPDATE … VALUES(col)` vs `ON CONFLICT (id) DO UPDATE SET … EXCLUDED.col`.
20
- * - the MONOTONIC de-publish guard (IMAGE-API-DESIGN.md §P2.10 / wq64gmm5e): `IF(status='published', old,
21
- * new)` vs `CASE WHEN … status = 'published' THEN old ELSE new END` — status/visibility/tenant_id/signed
22
- * are ALL preserved on a live published row in BOTH dialects.
23
- * - JSON columns: TiDB `JSON.stringify` verbatim vs PG `pgSafeJsonStringify` + `$n::jsonb` casts (lossy —
24
- * this is an observation-layer blob, not a trust-boundary payload, so `pgSafeJsonStringify`'s U+FFFD
25
- * replacement is acceptable; codex R15).
26
- * - PG-only identity-axis validation (codex R17-H1/R19): `repo` (the `idFor` hash input + the `(repo,digest)`
27
- * unique-key equivalence premise) and `tenantId` reject unstorable bytes UP FRONT (`pgHasUnstorable` →
28
- * `PgUnstorableError`) rather than silently mangling them — a rewrite would fork the hash id from the
29
- * stored column (same `(repo,digest)` collides the unique key while `ON CONFLICT (id)` doesn't match,
30
- * wedging registration permanently). TiDB has no equivalent NUL/lone-surrogate constraint, so it stores
31
- * verbatim. Same-family scalar sanitize (tag/recipeGitSha/generatorVersion/supersedes — codex R16) is
32
- * PG-only for the same reason; identity columns (repo/tenantId/digest/manifestSha) are reject-not-rewrite.
33
- * - null-safe ordering trick: `ORDER BY (build_date IS NULL), build_date DESC` vs `ORDER BY build_date DESC
34
- * NULLS LAST` (same semantics, dialect-native syntax).
35
- * - capability-flag predicate: `JSON_EXTRACT(capabilities, '$.key') = true` vs `capabilities -> 'key' =
36
- * 'true'::jsonb` (NOT a `::boolean` cast, which hard-errors on non-boolean jsonb and aborts the query;
37
- * the jsonb equality yields false instead, matching MySQL's exclude-don't-throw semantics for a present
38
- * but non-boolean capability value, e.g. `nestedBuildMode`).
39
- * - affectedRows vs rowCount, BIGINT (`size_bytes`) read back as a STRING on PG (via SqlDriver / mapImageRow).
40
- * - schema ownership: TiDB DDL lives in tidb-pool.ts; the PG DDL is exported from HERE
41
- * (PG_IMAGE_INDEX_SCHEMA + ensureSchema, self-contained for the integration test).
42
- */
43
1
  import { createHash } from "node:crypto";
44
2
  import { pgSafeJsonStringify, pgSanitizeText, pgHasUnstorable, PgUnstorableError } from "./pg-safe-json.js";
45
- import {
46
- // mapRow/ImageRow are the row-mapping surface shared by BOTH SQL twins — they live in the
47
- // backend-neutral store-contracts.ts (design/158 A3 沉底; no adapter imports its twin).
48
- mapImageRow as mapRow, } from "./store-contracts.js";
3
+ import { mapImageRow as mapRow, } from "./store-contracts.js";
49
4
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
50
- /** PG translation of the `sandbox_image_index` DDL in tidb-pool.ts (JSON→JSONB, DATETIME(3)→TIMESTAMPTZ(3),
51
- * TINYINT(1)→SMALLINT(布尔列 PG 映射归一,clay 批 2026-07-26 —— 两方言读回同为 number,写入 0/1,
52
- * 读侧 `!!` 保持,出口 JS 类型不变), inline UNIQUE KEY/KEY → separate CREATE UNIQUE INDEX / CREATE INDEX). */
53
5
  export const PG_IMAGE_INDEX_SCHEMA = [
54
6
  `CREATE TABLE IF NOT EXISTS sandbox_image_index (
55
7
  id VARCHAR(64) COLLATE "C" NOT NULL,
@@ -80,14 +32,12 @@ export const PG_IMAGE_INDEX_SCHEMA = [
80
32
  `CREATE INDEX IF NOT EXISTS idx_sandbox_image_index_digest ON sandbox_image_index (digest)`,
81
33
  `CREATE INDEX IF NOT EXISTS idx_sandbox_image_index_tenant ON sandbox_image_index (tenant_id, status)`,
82
34
  ];
83
- /** Idempotent self-contained schema apply (for the integration test; central aggregation is done separately). */
84
35
  export async function ensureSchema(pool) {
85
36
  for (const stmt of PG_IMAGE_INDEX_SCHEMA)
86
37
  await pool.query(stmt);
87
38
  }
88
39
  const MAX_LIMIT = 200;
89
40
  const DEFAULT_LIMIT = 50;
90
- /** `(visibility='public' OR tenant_id=?)` unless operator. TiDB form — `?` needs no index bookkeeping. */
91
41
  function visibilityClauseTidb(viewer) {
92
42
  if (viewer.operator)
93
43
  return { sql: "1=1", params: [] };
@@ -95,7 +45,6 @@ function visibilityClauseTidb(viewer) {
95
45
  return { sql: "(visibility='public' OR tenant_id=?)", params: [viewer.tenantId] };
96
46
  return { sql: "visibility='public'", params: [] };
97
47
  }
98
- /** PG form — positional `$n`, with a running index the caller continues from (PG has no anonymous `?`). */
99
48
  function visibilityClausePg(viewer, startIdx) {
100
49
  if (viewer.operator)
101
50
  return { sql: "1=1", params: [], nextIdx: startIdx };
@@ -121,7 +70,6 @@ function decodeCursor(c) {
121
70
  return null;
122
71
  }
123
72
  }
124
- /** TiDB upsert/registerBuilding params — the array literal is IDENTICAL text at both TiDB call sites. */
125
73
  function tidbUpsertParams(e, id, now) {
126
74
  return [
127
75
  id, e.profile, JSON.stringify(e.bands ?? []), e.repo, e.tag, e.digest,
@@ -136,17 +84,12 @@ const PG_UPSERT_COLS = "(id, profile, bands, repo, tag, digest, toolchain_versio
136
84
  "status, visibility, tenant_id, manifest_sha, recipe_git_sha, generator_version, build_date, supersedes, " +
137
85
  "signed, created_at, updated_at)";
138
86
  const PG_UPSERT_VALUES = "($1,$2,$3::jsonb,$4,$5,$6,$7::jsonb,$8::jsonb,$9::jsonb,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21)";
139
- /** PG upsert/registerBuilding params — deep-sanitized JSON (observation-layer, R15) + same-family scalar
140
- * sanitize (R16); identity columns (repo/tenantId/digest/manifestSha) are reject-not-rewrite (validated by
141
- * the caller BEFORE this runs — see the `pgHasUnstorable` guards in `upsert`/`registerBuilding`). */
142
87
  function pgUpsertParams(e, id, now) {
143
88
  return [
144
89
  id,
145
90
  pgSanitizeText(e.profile),
146
- pgSafeJsonStringify(e.bands ?? []), // R15:元数据 jsonb 四位=观测层(嵌套值可含外部构建产物文本)
147
- // codex R16:register 面标量只做类型检查——tag/tenantId/generatorVersion/supersedes 可携脏字节,
148
- // 同族观测标量一并 sanitize(digest/manifestSha 恒 hex 恒等;identity 值 sanitize 恒等=零行为差)。
149
- e.repo, // R17-H1:repo=身份(idFor 哈希输入)——入口拒绝式,列存原文(见 upsert/registerBuilding)
91
+ pgSafeJsonStringify(e.bands ?? []),
92
+ e.repo,
150
93
  pgSanitizeText(e.tag),
151
94
  e.digest,
152
95
  pgSafeJsonStringify(e.toolchainVersions ?? {}),
@@ -155,7 +98,7 @@ function pgUpsertParams(e, id, now) {
155
98
  e.sizeBytes,
156
99
  e.status,
157
100
  e.visibility,
158
- e.tenantId ?? null, // R19:tenantId=租户身份轴——入口拒绝式(改写=可见性归属漂移),列存原文
101
+ e.tenantId ?? null,
159
102
  e.manifestSha ?? null,
160
103
  e.recipeGitSha == null ? null : pgSanitizeText(e.recipeGitSha),
161
104
  e.generatorVersion == null ? null : pgSanitizeText(e.generatorVersion),
@@ -166,33 +109,18 @@ function pgUpsertParams(e, id, now) {
166
109
  now,
167
110
  ];
168
111
  }
169
- /** Dual-dialect INDEX store. See the file header for the dialect-delta ledger. */
170
112
  export class SqlImageIndex {
171
113
  db;
172
114
  constructor(db) {
173
115
  this.db = db;
174
116
  }
175
- /** Pick the dialect's SQL text. Both statements stay written out at the call site ON PURPOSE. */
176
117
  q(tidb, pg) {
177
118
  return this.db.dialect === "tidb" ? tidb : pg;
178
119
  }
179
- /** Stable index id = sha256(repo@digest) — identity is the digest, not the mutable tag. */
180
120
  static idFor(repo, digest) {
181
121
  return createHash("sha256").update(`${repo}@${digest}`).digest("hex").slice(0, 64);
182
122
  }
183
- /**
184
- * Register or update an index row (idempotent on repo+digest). Returns the row id.
185
- *
186
- * @deprecated #193 车7(staleness-sweep P2-11):**生产零调用**——曾声称的 operator
187
- * `/v1/images/register` 路径实际走 `registerBuilding`(routes/images.ts),本方法只剩集成测试当
188
- * seed 口在用。危险语义原样保留着:UNCONDITIONAL upsert 会把 `published` 行打回 `building`
189
- * (正是 §P2.10 静默 de-publish 病的成因,`registerBuilding` 因此才带 STATUS-MONOTONIC 守卫)。
190
- * **禁止新增生产调用**;测试 seed 请优先走 `registerBuilding`(不需要覆盖语义时)。留而不删的
191
- * 唯一理由=16 处 pg 集成测试 seed 依赖,整删随该套件下次翻新一并做。
192
- */
193
123
  async upsert(e) {
194
- // 🔴 PG-only identity-axis validation (codex R17-H1/R19) — TiDB has no NUL/lone-surrogate column
195
- // constraint, so it stores repo/tenantId verbatim; PG rejects up front (see file-header dialect delta).
196
124
  if (this.db.dialect === "pg") {
197
125
  if (pgHasUnstorable(e.repo))
198
126
  throw new PgUnstorableError("image repo (identity)");
@@ -224,32 +152,7 @@ export class SqlImageIndex {
224
152
  supersedes = EXCLUDED.supersedes, signed = EXCLUDED.signed, updated_at = EXCLUDED.updated_at`), this.db.dialect === "tidb" ? tidbUpsertParams(e, id, now) : pgUpsertParams(e, id, now));
225
153
  return id;
226
154
  }
227
- /**
228
- * STATUS-MONOTONIC register (IMAGE-API-DESIGN.md §P2.10 MUST-FIX) — the auto-register path the bake runner uses.
229
- *
230
- * `upsert` does an UNCONDITIONAL upsert: a re-ingested `done` (at-least-once from the runner) or a P2 bake of
231
- * a digest P3 already PROMOTED would flip a live `published` row back to `building`, and `latestPublished`
232
- * (`WHERE status='published'`) then DE-PUBLISHES a live, in-use image → sandbox-create 404s. This guarded path
233
- * NEVER downgrades a `published` row: on a fresh row it inserts (`status:'building'` by default); on an
234
- * existing row it updates the manifest projection + bumps `updated_at` but writes `status` only when the row
235
- * isn't already `published` — a late/duplicate `done` for an already-promoted digest is a no-op on the
236
- * lifecycle, never a regression. Returns the row id.
237
- *
238
- * Distinct from the unconditional `upsert` (#193 车7 更正:那条 operator `/v1/images/register` 路径
239
- * **也走本方法**——routes/images.ts:446 `idx.registerBuilding(...)`;`upsert` 已 @deprecated,只剩
240
- * 测试 seed 在用)。This is the bake auto-register's ONLY write into the index. Default status is `building` (P2 registers
241
- * node-local/quarantine `building` artifacts; P3's verify gate is what flips `building → published`).
242
- *
243
- * The `IF(...published...)` (TiDB) / `CASE WHEN...published...` (PG) guards are the monotonic de-publish
244
- * defense: once a row is `published` and live, a re-register must NOT downgrade `status` NOR flip the
245
- * columns `latestPublished` filters on. wq64gmm5e: guarding only `status` left `visibility`/`tenant_id`
246
- * (and `signed`) overwritable, so a re-register with visibility=private/tenant or a different tenant_id
247
- * silently dropped a live row out of every viewer's `latestPublished` — an effective de-publish. ALL FOUR
248
- * (status/visibility/tenant_id/signed) are gated identically in BOTH dialects; non-published rows still
249
- * take the freshest projection.
250
- */
251
155
  async registerBuilding(e) {
252
- // 🔴 PG-only identity-axis validation (codex R17-H1/R19) — same rationale as `upsert` above.
253
156
  if (this.db.dialect === "pg") {
254
157
  if (pgHasUnstorable(e.repo))
255
158
  throw new PgUnstorableError("image repo (identity)");
@@ -295,15 +198,10 @@ export class SqlImageIndex {
295
198
  const { rows } = await this.db.query(this.q("SELECT * FROM sandbox_image_index WHERE id=? LIMIT 1", "SELECT * FROM sandbox_image_index WHERE id = $1 LIMIT 1"), [id]);
296
199
  return rows[0] ? mapRow(rows[0]) : null;
297
200
  }
298
- /** By immutable digest — what a running sandbox was built from. */
299
201
  async getByDigest(digest) {
300
202
  const { rows } = await this.db.query(this.q("SELECT * FROM sandbox_image_index WHERE digest=? ORDER BY updated_at DESC LIMIT 1", "SELECT * FROM sandbox_image_index WHERE digest = $1 ORDER BY updated_at DESC LIMIT 1"), [digest]);
301
203
  return rows[0] ? mapRow(rows[0]) : null;
302
204
  }
303
- /** The newest PUBLISHED, viewer-visible digest for a profile — the `select` resolution.
304
- * 🔴 genuinely-divergent shape (not just SQL text): TiDB's `visibilityClauseTidb` needs no placeholder
305
- * index (anonymous `?`); PG's `visibilityClausePg` threads a running `$n` index the caller continues
306
- * from. Also the null-safe ORDER BY trick differs ((build_date IS NULL) vs NULLS LAST) — same semantics. */
307
205
  async latestPublished(profile, viewer) {
308
206
  if (this.db.dialect === "tidb") {
309
207
  const vis = visibilityClauseTidb(viewer);
@@ -318,13 +216,6 @@ export class SqlImageIndex {
318
216
  ORDER BY build_date DESC NULLS LAST, created_at DESC LIMIT 1`, [profile, ...vis.params]);
319
217
  return rows[0] ? mapRow(rows[0]) : null;
320
218
  }
321
- /**
322
- * Paginated, viewer-scoped catalog query (the UI's list). Keyset cursor on (created_at, id) desc.
323
- * 🔴 genuinely-divergent shape: the PG arm threads an explicit `$n` index through every optional predicate
324
- * (node-pg has no anonymous placeholder); the TiDB arm just appends `?` in call order. Kept as two full
325
- * branches rather than a shared placeholder-numbering helper (sql-driver.ts STEP 3/4) — a reviewer sees each
326
- * dialect's real query shape, not a generated abstraction over it.
327
- */
328
219
  async list(filter) {
329
220
  const limit = Math.min(Math.max(1, filter.limit ?? DEFAULT_LIMIT), MAX_LIMIT);
330
221
  let rawRows;
@@ -341,7 +232,6 @@ export class SqlImageIndex {
341
232
  params.push(filter.status);
342
233
  }
343
234
  if (filter.capability) {
344
- // JSON boolean true for the capability key.
345
235
  where.push("JSON_EXTRACT(capabilities, ?) = true");
346
236
  params.push(`$.${filter.capability}`);
347
237
  }
@@ -371,11 +261,6 @@ export class SqlImageIndex {
371
261
  params.push(filter.status);
372
262
  }
373
263
  if (filter.capability) {
374
- // JSON boolean true for the capability key. Compare at the jsonb layer (NOT a ::boolean cast): a
375
- // `::boolean` cast HARD-ERRORS on any non-boolean jsonb value (string/number/JSON null) and aborts the
376
- // whole query, whereas `= 'true'::jsonb` yields false for those — faithfully reproducing MySQL's
377
- // `JSON_EXTRACT(...) = true` semantics (a present-but-non-boolean cap, e.g.
378
- // nestedBuildMode="rootless-buildkit", simply excludes the row).
379
264
  where.push(`capabilities -> $${idx++} = 'true'::jsonb`);
380
265
  params.push(filter.capability);
381
266
  }
@@ -401,13 +286,11 @@ export class SqlImageIndex {
401
286
  nextCursor = encodeCursor({ createdAt: last.createdAt, id: last.id });
402
287
  }
403
288
  if (filter.latestOnly) {
404
- // collapse to the newest row per profile (rows are already build/created-desc within the page).
405
289
  const seen = new Set();
406
290
  entries = entries.filter((e) => (seen.has(e.profile) ? false : (seen.add(e.profile), true)));
407
291
  }
408
292
  return { entries, nextCursor };
409
293
  }
410
- /** Lifecycle transition (atomic-publish: building→published, or →failed/deprecated). */
411
294
  async setStatus(id, status, opts) {
412
295
  if (this.db.dialect === "tidb") {
413
296
  const sets = ["status=?", "updated_at=?"];
@@ -431,13 +314,11 @@ export class SqlImageIndex {
431
314
  await this.db.query(`UPDATE sandbox_image_index SET ${sets.join(", ")} WHERE id = $${idx}`, params);
432
315
  }
433
316
  }
434
- /** MySQL-protocol (TiDB) binding — historical class name + ctor shape preserved. */
435
317
  export class TiDBImageIndex extends SqlImageIndex {
436
318
  constructor(pool) {
437
319
  super(mysqlDriver(pool));
438
320
  }
439
321
  }
440
- /** PostgreSQL binding — historical class name + ctor shape preserved. */
441
322
  export class PgImageIndex extends SqlImageIndex {
442
323
  constructor(pool) {
443
324
  super(pgDriver(pool));
@@ -1,36 +1,13 @@
1
- /**
2
- * design/103 background-shell pod-side scripts (pure, exported for testing) + design/116 detach-capable exec —
3
- * the k8s adapter's text generation/parsing layer for background shells (`buildBg*Script` ×5 +
4
- * `buildDetachCapableExec` + `parseBgPollOutput`). Split out of `remote-env-k8s.ts` (design/158 A13,
5
- * internal-lossless — `remote-env-k8s.ts` re-exports every symbol below unchanged).
6
- *
7
- * The pod-side runner is `setsid`'d into its OWN session so the whole job (cmd + its grandchildren) shares one
8
- * process group we can group-kill (`kill -<pgid>`); the session leader self-reports its pgid (`$$`) so we never
9
- * depend on `setsid &`'s `$!` (which is the short-lived parent). The command writes DIRECTLY to pod files (out/err),
10
- * re-read by byte cursor each poll (path ① — survives the consume-once exec seam).
11
- *
12
- * 🔴 Why DIRECT redirect (`> out 2> err`), not a FIFO+`head` cap: on-box kata verification proved a FIFO+`head`
13
- * pipeline block-buffers `head`'s stdout → the `out` file stays EMPTY until head's stdio buffer fills (KBs) or EOF,
14
- * so `pollBackground` sees nothing for a slow streamer (the whole dev-server use case). Direct redirect makes the
15
- * shell write each `echo` straight to the file → immediately pollable (the command's OWN buffering still applies,
16
- * same as TOC/E2B's pipe). DISK bound: the pod has no native ephemeral cap, so a runaway producer is bounded by
17
- * (a) the BG-timeout hard wall + pod `activeDeadlineSeconds`, and (b) a control-plane size guard in the driver's
18
- * read() that reaps a job whose output exceeds `K8S_BG_FILE_CAP` (kata VM isolation already contains the blast to
19
- * this one disposable pod). per-poll TRANSFER stays bounded by the readCap in buildBgPollScript regardless.
20
- */
21
1
  import { shellQuote } from "./remote-shell.js";
22
- /** The detached runner (base64'd into the pod, run under `setsid`). Self-reports pgid; streams output to out/err. */
23
2
  export function buildBgRunnerScript(dir) {
24
3
  const q = (name) => shellQuote(`${dir}/${name}`);
25
4
  return [
26
- // 🔴 self-report the pgid: $$ is this (session-leader) shell's pid == the session/group id. Atomic write.
27
5
  `echo $$ > ${q("pgid.tmp")} 2>/dev/null && mv ${q("pgid.tmp")} ${q("pgid")} 2>/dev/null`,
28
- `sh ${q("cmd.sh")} > ${q("out")} 2> ${q("err")} < /dev/null`, // direct redirect → streaming-visible output
6
+ `sh ${q("cmd.sh")} > ${q("out")} 2> ${q("err")} < /dev/null`,
29
7
  `__RC=$?`,
30
- `echo $__RC > ${q("exit.tmp")} 2>/dev/null && mv ${q("exit.tmp")} ${q("exit")} 2>/dev/null`, // exit sentinel = atomic
8
+ `echo $__RC > ${q("exit.tmp")} 2>/dev/null && mv ${q("exit.tmp")} ${q("exit")} 2>/dev/null`,
31
9
  ].join("\n");
32
10
  }
33
- /** Short foreground launcher: writes cmd.sh + runner.sh, then `setsid`s the runner detached and returns immediately. */
34
11
  export function buildBgLauncherScript(dir, cmdShB64, runnerShB64) {
35
12
  const d = shellQuote(dir);
36
13
  const q = (name) => shellQuote(`${dir}/${name}`);
@@ -38,28 +15,14 @@ export function buildBgLauncherScript(dir, cmdShB64, runnerShB64) {
38
15
  `mkdir -p ${d} && chmod 700 ${d}`,
39
16
  `printf %s '${cmdShB64}' | base64 -d > ${q("cmd.sh")}`,
40
17
  `printf %s '${runnerShB64}' | base64 -d > ${q("runner.sh")}`,
41
- // detach into a new session (survives the exec socket close — disposable-pod posture / startBackground precedent)
42
18
  `setsid sh ${q("runner.sh")} >/dev/null 2>&1 &`,
43
- // 🔴 Do NOT report launched until the runner has self-reported its pgid — otherwise a killBackground
44
- // / timeout landing in the gap finds no pgid, no-ops, yet the manager marks the shell killed → a LEAKED process
45
- // group. Bounded (~2s); the runner writes pgid as its very FIRST action, so this is one iteration in practice.
46
19
  `__i=0; while [ ! -s ${q("pgid")} ] && [ $__i -lt 40 ]; do __i=$((__i+1)); sleep 0.05; done`,
47
- // 🔴 (workflow MED) Only report launched if the runner actually started (pgid present). If setsid is missing or the
48
- // runner crashed before writing pgid, report failure → the driver surfaces spawn_failed instead of a phantom
49
- // "running" shell that never ends until the BG timeout.
50
20
  `if [ -s ${q("pgid")} ]; then echo SEMABG_LAUNCHED; else echo SEMABG_NOPGID; fi`,
51
21
  ].join("\n");
52
22
  }
53
- /** Poll script: emit totals + liveness + exit, then the new bytes per stream (base64, bounded by `readCap`). */
54
23
  export function buildBgPollScript(dir, stdoutCursor, stderrCursor, readCap) {
55
24
  const d = shellQuote(dir);
56
- const slice = (file, cursor) =>
57
- // 🔴 (workflow MED) Read is bounded to the wc SNAPSHOT (__T), not to the live EOF — `head -c` caps the tail so a
58
- // producer writing between `wc -c` and `tail` cannot return bytes past __T (the driver advances the cursor to __T,
59
- // so reading past it would DUPLICATE next poll AND defeat the readCap transfer bound). ≤readCap new bytes → the
60
- // whole increment [cursor, __T); otherwise the LAST readCap bytes [__T-readCap, __T) (recent tail, like TOC) and
61
- // the control plane records the gap as bytesDroppedBeforeCursor.
62
- [
25
+ const slice = (file, cursor) => [
63
26
  `__N=$((__T - ${cursor}))`,
64
27
  `if [ "$__N" -gt 0 ]; then`,
65
28
  ` if [ "$__N" -le ${readCap} ]; then tail -c +$(( ${cursor} + 1 )) ${d}/${file} 2>/dev/null | head -c "$__N" | base64;`,
@@ -68,8 +31,6 @@ export function buildBgPollScript(dir, stdoutCursor, stderrCursor, readCap) {
68
31
  ].join("\n");
69
32
  return [
70
33
  `if [ ! -d ${d} ]; then echo SEMABG_GONE; exit 0; fi`,
71
- // `$(( ... + 0 ))` normalizes the byte count: it both defaults empty→0 AND strips the leading whitespace that
72
- // BSD/macOS `wc -c < file` emits (GNU/Debian pods don't pad, but keep the META line parseable on either).
73
34
  `__OT=$(wc -c < ${d}/out 2>/dev/null); __OT=$(( \${__OT:-0} + 0 ))`,
74
35
  `__ET=$(wc -c < ${d}/err 2>/dev/null); __ET=$(( \${__ET:-0} + 0 ))`,
75
36
  `__EX=$( [ -f ${d}/exit ] && cat ${d}/exit 2>/dev/null || echo - )`,
@@ -85,33 +46,15 @@ export function buildBgPollScript(dir, stdoutCursor, stderrCursor, readCap) {
85
46
  `echo SEMABG_X`,
86
47
  ].join("\n");
87
48
  }
88
- /** KillShell: graceful SIGTERM to the group, then a forced SIGKILL (idempotent — a dead group just errors out).
89
- * The `|| kill <pid>` fallback covers an ADOPTED exec whose pgid prelude self-reported `$$` without a `setsid`
90
- * (whether the k8s-exec'd sh is a group leader is runtime-dependent); a launch-mode runner is always setsid'd,
91
- * so its group kill succeeds and the fallback never fires there. */
92
49
  export function buildBgKillScript(dir) {
93
50
  const d = shellQuote(dir);
94
51
  return [
95
- // 🔴 review LOW (Fable-5 #4 tightened): an ADOPTED shell (create-then-abort detach) can be killed in the
96
- // sub-second window BEFORE its detach-capable exec even ran `mkdir` — let alone forked the job + wrote pgid.
97
- // A bare read (or a dir-existence gate: the dir does not exist YET in that window, indistinguishable from
98
- // "already ended + EXIT-trap-removed") would skip the kill yet report SEMABG_KILLED — a silent no-op, the
99
- // command escapes the wall. So wait UNCONDITIONALLY for the pgid file (bounded ~2s, mirroring the LAUNCH
100
- // path's pre-report pgid gate). Cost of the removed dir-gate: a kill racing the command's natural end (dir
101
- // already EXIT-trap-removed) idles the full ~2s before no-op'ing — acceptable, because driver.kill only runs
102
- // while the manager still believes the shell is running (that overlap window is sub-second), and the pod is
103
- // disposable. For a launch-mode shell pgid is already present → first iteration exits (no-op).
104
52
  `__i=0; while [ ! -s ${d}/pgid ] && [ $__i -lt 40 ]; do __i=$((__i+1)); sleep 0.05; done`,
105
53
  `__PG=$(cat ${d}/pgid 2>/dev/null); __PG=\${__PG:--}`,
106
54
  `if [ "$__PG" != "-" ]; then kill -TERM -"$__PG" 2>/dev/null || kill -TERM "$__PG" 2>/dev/null; sleep 1; kill -KILL -"$__PG" 2>/dev/null || kill -KILL "$__PG" 2>/dev/null; fi`,
107
55
  `echo SEMABG_KILLED`,
108
56
  ].join("\n");
109
57
  }
110
- /** dispose: forced SIGKILL to the group + remove the job dir (no grace — teardown). Best-effort.
111
- * ⚠️ Deliberately NO pgid-wait (unlike {@link buildBgKillScript}, review #4): a create-then-abort adopt disposed
112
- * in the pre-mkdir window escapes THIS script — but every dispose path ends in pod deletion (task-end destroy;
113
- * suspendVM deletes the pod after the snapshot), so the escape is pod-lifetime-bounded, and a wait here would
114
- * add 2s × N shells to the suspend/teardown path for a sub-second window. Kill (the model-facing wall) waits. */
115
58
  export function buildBgDisposeScript(dir) {
116
59
  const d = shellQuote(dir);
117
60
  return [
@@ -121,54 +64,27 @@ export function buildBgDisposeScript(dir) {
121
64
  `echo SEMABG_DISPOSED`,
122
65
  ].join("\n");
123
66
  }
124
- /**
125
- * design/116 detach: wrap a foreground exec so it is ADOPTABLE mid-flight, running the command as a real
126
- * process-group LEADER so an adopted shell's KillShell / BG hard wall reap the WHOLE subtree.
127
- *
128
- * 🔴 review MED (why `setsid`, not bare `$$`): the wrapper `sh` that `kubectl exec` starts has a RUNTIME-DEPENDENT
129
- * process group — under many container runtimes it is NOT its own group leader (its pgid is some ancestor's), so
130
- * writing `$$` as the pgid and later `kill -$$` targets a group that does not exist → the fallback `kill $$` reaps
131
- * only the wrapper, orphaning the command's children PAST the §3.6 hard wall (they keep streaming into the adopted
132
- * shell's buffer until the pod's `activeDeadlineSeconds`). `setsid` starts the command in a NEW session ⇒ a new
133
- * process group whose leader pid IS the backgrounded job pid, so `kill -<pid>` reaps the command and every
134
- * descendant. The command's stdout/stderr fds (the exec socket's channels) are INHERITED across setsid — only the
135
- * controlling terminal is dropped (there is none: kubectl-exec here is non-tty) — so output/exit still flow on the
136
- * SAME WebSocket, which IS the adopted shell's feed. `wait` propagates the real exit code.
137
- *
138
- * The command is base64'd into a pod-side `cmd.sh` (no quoting hazard, mirrors the launch path). The pgid is written
139
- * atomically (tmp+mv) AFTER the job forks, so a poll never reads a half-written id. Every step is best-effort: a pod
140
- * without `setsid` (rare on the kata base image) degrades to the `$$` path (the pre-fix behavior), and a read-only
141
- * /tmp degrades to a pgid-less adopt (kill no-ops; the disposable pod's teardown reaps) — never a broken exec. The
142
- * EXIT trap removes the dir when the command ends normally, so non-detached execs don't litter the pod.
143
- */
144
67
  export function buildDetachCapableExec(dir, command) {
145
68
  const d = shellQuote(dir);
146
69
  const q = (name) => shellQuote(`${dir}/${name}`);
147
70
  const cmdB64 = Buffer.from(command, "utf8").toString("base64");
148
71
  const writePgid = (val) => `printf %s ${val} > ${q("pgid.tmp")} 2>/dev/null && mv ${q("pgid.tmp")} ${q("pgid")} 2>/dev/null`;
149
72
  return [
150
- // Decode the command into a VARIABLE (not a pod-side file): a read-only-/tmp degrade must still RUN the command
151
- // (review's degrade-safety case) — a file target under an un-creatable dir would leave nothing to run. `sh -c
152
- // "$__cmd"` is byte-equivalent to sourcing a cmd.sh whose content is $__cmd. base64 has no quoting hazard.
153
73
  `__cmd=$(printf %s '${cmdB64}' | base64 -d)`,
154
74
  `mkdir -p ${d} 2>/dev/null`,
155
75
  `trap 'rm -rf ${d} 2>/dev/null' EXIT`,
156
76
  `if command -v setsid >/dev/null 2>&1; then`,
157
- // setsid backgrounds the job as a new-session group leader; $! is that leader's pid == its pgid. Output fds are
158
- // inherited (the exec socket), only the tty is dropped. `wait` blocks the wrapper until the job ends (or is
159
- // group-killed) and yields its exit code — so exec/adopt semantics are unchanged, only the reap target is fixed.
160
77
  ` setsid sh -c "$__cmd" &`,
161
78
  ` __pg=$!`,
162
79
  ` ${writePgid('"$__pg"')}`,
163
80
  ` wait "$__pg"; __rc=$?`,
164
81
  `else`,
165
- ` ${writePgid('"$$"')}`, // no setsid → best-effort $$ (pre-fix behavior; the pod base image ships setsid)
82
+ ` ${writePgid('"$$"')}`,
166
83
  ` sh -c "$__cmd"; __rc=$?`,
167
84
  `fi`,
168
85
  `exit $__rc`,
169
86
  ].join("\n");
170
87
  }
171
- /** Parse {@link buildBgPollScript} stdout. Markers contain `_` (absent from the base64 alphabet) so they never collide. */
172
88
  export function parseBgPollOutput(raw) {
173
89
  if (raw.includes("SEMABG_GONE")) {
174
90
  return { gone: true, outTotal: 0, errTotal: 0, alive: false, stdout: "", stdoutBytes: 0, stderr: "", stderrBytes: 0 };
@@ -1,26 +1,7 @@
1
- /**
2
- * The confined, ADDITIVE pod-spec patch ({@link PodSpecPatch}/{@link applyPodSpecPatch}) + the exec-socket TLS
3
- * shape ({@link execSocketTlsOptions}) + the v4 channel status-frame parse ({@link exitCodeFromStatus}) — the
4
- * k8s adapter's protocol/patch layer, independent of pod lifecycle. Split out of `remote-env-k8s.ts` (design/158
5
- * A13, internal-lossless — `remote-env-k8s.ts` re-exports every symbol below unchanged).
6
- */
7
- /**
8
- * Apply a {@link PodSpecPatch} to a sandbox pod object IN PLACE — purely ADDITIVE. Volumes + volumeMounts are
9
- * APPENDED (never replaced); securityContext objects are shallow-MERGED onto any existing; labels are merged. It
10
- * deliberately touches ONLY spec.volumes / spec.securityContext / containers[0].{volumeMounts,securityContext} /
11
- * metadata.labels — so the hard isolation invariants (runtimeClassName, automountServiceAccountToken,
12
- * restartPolicy, activeDeadlineSeconds, image, command) are preserved by construction. Pure + exported for testing.
13
- */
14
1
  export function applyPodSpecPatch(pod, patch) {
15
- // 🔴 wq64gmm5e: a securityContext patch is a HARDENING seam — it may only TIGHTEN, never weaken the isolation
16
- // the L8 gradeP1 premise depends on. A blind spread let privileged:true / allowPrivilegeEscalation:true /
17
- // capabilities.add[SYS_ADMIN] / readOnlyRootFilesystem:false through verbatim (and the "cannot weaken" test was
18
- // fake-green). Fail CLOSED on any privilege-escalating key so the docstring claim is actually enforced.
19
2
  assertHardeningOnly(patch.podSecurityContext, "podSecurityContext");
20
3
  assertHardeningOnly(patch.containerSecurityContext, "containerSecurityContext");
21
4
  if (patch.labels) {
22
- // Additive + identity-preserving: merge the patch labels, then re-assert the base identity labels LAST so a
23
- // patch can never clobber the app/managed-by labels the RBAC selectors + reaper key on (wq64gmm5e).
24
5
  const identity = {};
25
6
  for (const k of ["app", "managed-by"]) {
26
7
  const v = pod.metadata.labels?.[k];
@@ -30,12 +11,6 @@ export function applyPodSpecPatch(pod, patch) {
30
11
  pod.metadata.labels = { ...pod.metadata.labels, ...patch.labels, ...identity };
31
12
  }
32
13
  const spec = pod.spec;
33
- // 🔴 Volumes are the SAME isolation-breach class as a privileged securityContext (review: workflow HIGH). Kata
34
- // passes a hostPath through into the guest VM (virtio-fs/9p host share — NOT blocked by the VM boundary), so a
35
- // `hostPath:{path:"/"}` volume mounts the k3s NODE root (containerd sock, other pods, host secrets) into the
36
- // sandbox — defeating the L8 immutable-oracle integrity premise as completely as privileged:true. Reject any
37
- // host-backed volume source; only pod-local sources (emptyDir/configMap/secret/projected/downwardAPI/ephemeral)
38
- // are additive-safe.
39
14
  assertVolumesPodLocal(patch.volumes, "volumes");
40
15
  if (patch.volumes?.length)
41
16
  spec.volumes = [...(spec.volumes ?? []), ...patch.volumes];
@@ -49,8 +24,6 @@ export function applyPodSpecPatch(pod, patch) {
49
24
  container.securityContext = { ...(container.securityContext ?? {}), ...patch.containerSecurityContext };
50
25
  }
51
26
  }
52
- /** Fail-closed direction guard for a securityContext patch: reject any key that WEAKENS isolation. A patch may
53
- * drop capabilities + tighten, never escalate. Throws (caller surfaces as a provisioning failure). */
54
27
  function assertHardeningOnly(sc, where) {
55
28
  if (!sc)
56
29
  return;
@@ -70,16 +43,12 @@ function assertHardeningOnly(sc, where) {
70
43
  const caps = sc.capabilities;
71
44
  if (caps && Array.isArray(caps.add) && caps.add.length > 0)
72
45
  bad(`capabilities.add ${JSON.stringify(caps.add)} (a hardening patch may only .drop)`);
73
- // procMount:"Unmasked" re-exposes the masked /proc paths; seccompProfile:"Unconfined" disables syscall filtering —
74
- // both WEAKEN confinement (review: workflow HIGH secondary), so they belong on the fail-closed list.
75
46
  if (sc.procMount === "Unmasked")
76
47
  bad('procMount:"Unmasked" (re-exposes masked /proc)');
77
48
  const seccomp = sc.seccompProfile;
78
49
  if (seccomp && seccomp.type === "Unconfined")
79
50
  bad('seccompProfile.type:"Unconfined" (disables syscall filtering)');
80
51
  }
81
- /** Fail-closed guard for patch volumes: a Kata guest shares hostPath/csi/flexVolume sources from the NODE, so only
82
- * POD-LOCAL sources are additive-safe. Reject anything host-backed. Throws (caller surfaces as a provisioning fail). */
83
52
  function assertVolumesPodLocal(volumes, where) {
84
53
  if (!volumes?.length)
85
54
  return;
@@ -94,19 +63,10 @@ function assertVolumesPodLocal(volumes, where) {
94
63
  }
95
64
  }
96
65
  }
97
- /**
98
- * TLS options for the exec WebSocket, shaped for BOTH runtimes (2026-06-11 in-cluster drill finding):
99
- * Bun's `ws` shim IGNORES node-ws's top-level `ca`/`rejectUnauthorized` (handshake fails against a
100
- * cluster CA even with rejectUnauthorized:false) and reads a Bun-specific `tls` sub-object instead;
101
- * Node's real `ws` uses the top-level keys and passes the unknown `tls` key harmlessly through to
102
- * tls.connect. One merged shape serves both. Live-proven on build-host (probe: top-level ca → TLS handshake
103
- * failed; tls.ca → OPEN).
104
- */
105
66
  export function execSocketTlsOptions(ca, insecureTls) {
106
67
  const caList = ca ? [ca] : undefined;
107
68
  return { ca: caList, rejectUnauthorized: !insecureTls, tls: { ca: caList, rejectUnauthorized: !insecureTls } };
108
69
  }
109
- /** Parse the v4.channel.k8s.io status frame (channel 3) into an exit code, or a typed failure. */
110
70
  export function exitCodeFromStatus(statusJson) {
111
71
  try {
112
72
  const s = JSON.parse(statusJson);
@@ -114,7 +74,6 @@ export function exitCodeFromStatus(statusJson) {
114
74
  return { exitCode: 0 };
115
75
  const cause = s.details?.causes?.find((c) => c.reason === "ExitCode");
116
76
  if (cause?.message?.trim()) {
117
- // trim-guard: Number("")/Number(" ") === 0 — an empty message must NOT read as a fake exit 0 on a Failure
118
77
  const code = Number(cause.message);
119
78
  if (Number.isFinite(code))
120
79
  return { exitCode: code };