@sema-agent/server 1.323.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (539) hide show
  1. package/dist/approval-hmac.d.ts +40 -0
  2. package/dist/approval-hmac.js +67 -0
  3. package/dist/approval.d.ts +31 -0
  4. package/dist/approval.js +72 -4
  5. package/dist/audit.d.ts +26 -0
  6. package/dist/audit.js +40 -1
  7. package/dist/auth-bridge.d.ts +10 -0
  8. package/dist/auth-bridge.js +47 -4
  9. package/dist/auth-keys.d.ts +19 -0
  10. package/dist/auth-keys.js +13 -0
  11. package/dist/bake-runner/main.d.ts +5 -0
  12. package/dist/bake-runner/main.js +43 -4
  13. package/dist/bake-runner/protocol.d.ts +57 -0
  14. package/dist/bake-runner/protocol.js +58 -3
  15. package/dist/bake-runner/runner.d.ts +54 -0
  16. package/dist/bake-runner/runner.js +87 -5
  17. package/dist/bench/l8/artifact.d.ts +63 -0
  18. package/dist/bench/l8/artifact.js +61 -2
  19. package/dist/bench/l8/escape.d.ts +36 -0
  20. package/dist/bench/l8/escape.js +25 -0
  21. package/dist/bench/l8/index.d.ts +14 -0
  22. package/dist/bench/l8/index.js +14 -0
  23. package/dist/bench/l8/probes.d.ts +74 -0
  24. package/dist/bench/l8/probes.js +60 -0
  25. package/dist/bench/l8/run-probes.d.ts +43 -0
  26. package/dist/bench/l8/run-probes.js +45 -2
  27. package/dist/bench/s1/arms.d.ts +126 -0
  28. package/dist/bench/s1/arms.js +188 -13
  29. package/dist/bench/s1/live-deps.d.ts +66 -0
  30. package/dist/bench/s1/live-deps.js +249 -11
  31. package/dist/bench/s1/oracle.d.ts +49 -0
  32. package/dist/bench/s1/oracle.js +36 -1
  33. package/dist/bench/s1/repair-oracle-adapter.d.ts +33 -0
  34. package/dist/bench/s1/repair-oracle-adapter.js +30 -0
  35. package/dist/bench/s1/reviewer.d.ts +38 -0
  36. package/dist/bench/s1/reviewer.js +34 -0
  37. package/dist/bench/s1/row.d.ts +77 -0
  38. package/dist/bench/s1/row.js +62 -0
  39. package/dist/bench/s1/run-firm.d.ts +47 -0
  40. package/dist/bench/s1/run-firm.js +70 -4
  41. package/dist/bench/s1/runner-ctx.d.ts +71 -0
  42. package/dist/bench/s1/runner-ctx.js +36 -0
  43. package/dist/bench/s1/tasks.d.ts +108 -0
  44. package/dist/bench/s1/tasks.js +115 -0
  45. package/dist/boot-reclaim.d.ts +19 -0
  46. package/dist/boot-reclaim.js +20 -0
  47. package/dist/brain.d.ts +25 -0
  48. package/dist/brain.js +61 -1
  49. package/dist/budget.d.ts +66 -0
  50. package/dist/budget.js +128 -4
  51. package/dist/capabilities/builtin-tools.d.ts +3 -0
  52. package/dist/capabilities/builtin-tools.js +3 -0
  53. package/dist/capabilities/center-plugins.d.ts +17 -1
  54. package/dist/capabilities/center-plugins.js +45 -4
  55. package/dist/capabilities/center-prompts.d.ts +46 -0
  56. package/dist/capabilities/center-prompts.js +57 -3
  57. package/dist/capabilities/code-review-council.d.ts +28 -0
  58. package/dist/capabilities/code-review-council.js +40 -7
  59. package/dist/capabilities/collab-wire.d.ts +2 -0
  60. package/dist/capabilities/collab-workflows.d.ts +7 -0
  61. package/dist/capabilities/collab-workflows.js +44 -1
  62. package/dist/capabilities/oa-tools.d.ts +6 -0
  63. package/dist/capabilities/oa-tools.js +10 -0
  64. package/dist/capabilities/prompt.d.ts +14 -0
  65. package/dist/capabilities/prompt.js +14 -0
  66. package/dist/capabilities/prompts/code-review.d.ts +14 -0
  67. package/dist/capabilities/prompts/code-review.js +14 -0
  68. package/dist/capabilities/prompts/identity.d.ts +6 -0
  69. package/dist/capabilities/prompts/identity.js +6 -0
  70. package/dist/capabilities/prompts/team.d.ts +4 -0
  71. package/dist/capabilities/prompts/team.js +4 -0
  72. package/dist/capabilities/repo-tools.d.ts +11 -0
  73. package/dist/capabilities/repo-tools.js +8 -0
  74. package/dist/capabilities/sandbox-file-send.d.ts +95 -0
  75. package/dist/capabilities/sandbox-file-send.js +91 -3
  76. package/dist/capabilities/scenarios.d.ts +130 -0
  77. package/dist/capabilities/scenarios.js +180 -7
  78. package/dist/capabilities/select-environment-tool.d.ts +7 -0
  79. package/dist/capabilities/select-environment-tool.js +30 -0
  80. package/dist/capabilities/send-user-file-tool.d.ts +33 -0
  81. package/dist/capabilities/send-user-file-tool.js +65 -1
  82. package/dist/capabilities/skills.d.ts +29 -0
  83. package/dist/capabilities/skills.js +13 -2
  84. package/dist/capabilities/team.d.ts +22 -0
  85. package/dist/capabilities/team.js +22 -2
  86. package/dist/capabilities/tool-defer.d.ts +26 -0
  87. package/dist/capabilities/tool-defer.js +3 -0
  88. package/dist/config-center/apply-effective.d.ts +65 -0
  89. package/dist/config-center/apply-effective.js +204 -12
  90. package/dist/config-center/http-client.d.ts +34 -0
  91. package/dist/config-center/http-client.js +68 -0
  92. package/dist/config-center/restart-signal.d.ts +22 -0
  93. package/dist/config-center/restart-signal.js +39 -0
  94. package/dist/config-center/skills-mcp.d.ts +20 -1
  95. package/dist/config-center/skills-mcp.js +42 -7
  96. package/dist/config-center/types.d.ts +92 -1
  97. package/dist/config-lkg.d.ts +12 -0
  98. package/dist/config-lkg.js +41 -0
  99. package/dist/config-provider.d.ts +78 -0
  100. package/dist/config-provider.js +165 -2
  101. package/dist/config-types.d.ts +546 -0
  102. package/dist/config.d.ts +64 -0
  103. package/dist/config.js +455 -60
  104. package/dist/degenerate-instrument.d.ts +38 -0
  105. package/dist/degenerate-instrument.js +55 -3
  106. package/dist/elicitation.d.ts +39 -0
  107. package/dist/elicitation.js +87 -3
  108. package/dist/env-facts.d.ts +61 -0
  109. package/dist/env-facts.js +73 -7
  110. package/dist/fleet/fleet-bus.d.ts +185 -0
  111. package/dist/fleet/fleet-bus.js +333 -27
  112. package/dist/fleet/subagent-tail-bus.d.ts +11 -0
  113. package/dist/fleet/subagent-tail-bus.js +33 -2
  114. package/dist/fleet-client.d.ts +38 -0
  115. package/dist/fleet-client.js +44 -9
  116. package/dist/fleet-lease.d.ts +46 -0
  117. package/dist/fleet-lease.js +73 -5
  118. package/dist/hooks/branch-transcript.d.ts +8 -0
  119. package/dist/hooks/branch-transcript.js +33 -0
  120. package/dist/hooks/cc-stop-prompt.d.ts +42 -0
  121. package/dist/hooks/cc-stop-prompt.js +43 -1
  122. package/dist/hooks/hook-llm.d.ts +15 -0
  123. package/dist/hooks/hook-llm.js +47 -0
  124. package/dist/hooks/hook-runner.d.ts +89 -0
  125. package/dist/hooks/hook-runner.js +327 -18
  126. package/dist/http/idempotency.d.ts +31 -0
  127. package/dist/http/idempotency.js +34 -0
  128. package/dist/http/principal-gate.d.ts +17 -0
  129. package/dist/http/principal-gate.js +35 -3
  130. package/dist/http/route-ctx.d.ts +36 -0
  131. package/dist/http/routes/approvals-assistant.d.ts +25 -0
  132. package/dist/http/routes/approvals-assistant.js +253 -23
  133. package/dist/http/routes/attachments.js +22 -4
  134. package/dist/http/routes/capabilities.d.ts +7 -0
  135. package/dist/http/routes/capabilities.js +202 -1
  136. package/dist/http/routes/fleet.d.ts +6 -0
  137. package/dist/http/routes/fleet.js +94 -5
  138. package/dist/http/routes/images.d.ts +17 -0
  139. package/dist/http/routes/images.js +222 -14
  140. package/dist/http/routes/leader.d.ts +7 -0
  141. package/dist/http/routes/leader.js +10 -0
  142. package/dist/http/routes/memory-policy.d.ts +6 -0
  143. package/dist/http/routes/memory-policy.js +38 -6
  144. package/dist/http/routes/notify-wake.d.ts +10 -0
  145. package/dist/http/routes/notify-wake.js +19 -3
  146. package/dist/http/routes/observability.d.ts +6 -0
  147. package/dist/http/routes/observability.js +19 -2
  148. package/dist/http/routes/runs.d.ts +15 -0
  149. package/dist/http/routes/runs.js +464 -41
  150. package/dist/http/routes/session-sync.d.ts +15 -0
  151. package/dist/http/routes/session-sync.js +229 -19
  152. package/dist/http/routes/sessions-list.d.ts +6 -0
  153. package/dist/http/routes/sessions-list.js +35 -4
  154. package/dist/http/routes/sessions.d.ts +14 -0
  155. package/dist/http/routes/sessions.js +176 -31
  156. package/dist/http/routes/side-query.d.ts +8 -0
  157. package/dist/http/routes/side-query.js +26 -1
  158. package/dist/http/routes/tasks.d.ts +18 -0
  159. package/dist/http/routes/tasks.js +501 -38
  160. package/dist/http/routes/trace-usage.d.ts +10 -0
  161. package/dist/http/routes/trace-usage.js +66 -14
  162. package/dist/http/routes/workflows.d.ts +12 -0
  163. package/dist/http/routes/workflows.js +180 -19
  164. package/dist/http/run-meta.d.ts +11 -0
  165. package/dist/http/run-meta.js +6 -0
  166. package/dist/http/send.d.ts +19 -0
  167. package/dist/http/send.js +11 -0
  168. package/dist/http/server.d.ts +255 -0
  169. package/dist/http/server.js +921 -75
  170. package/dist/http/sse-log.d.ts +15 -0
  171. package/dist/http/sse-log.js +16 -0
  172. package/dist/http/tar.d.ts +9 -0
  173. package/dist/http/tar.js +21 -5
  174. package/dist/http/wire-gate.d.ts +9 -0
  175. package/dist/http/wire-gate.js +9 -0
  176. package/dist/http/wire-types.d.ts +187 -0
  177. package/dist/http/workspace-content.d.ts +8 -0
  178. package/dist/http/workspace-content.js +10 -0
  179. package/dist/images/bake-validate.d.ts +63 -0
  180. package/dist/images/bake-validate.js +71 -1
  181. package/dist/images/manifest.d.ts +19 -0
  182. package/dist/images/manifest.js +6 -1
  183. package/dist/index.d.ts +1 -5
  184. package/dist/index.js +10 -5
  185. package/dist/key-resolver.d.ts +23 -0
  186. package/dist/key-resolver.js +27 -2
  187. package/dist/leader/diffout.d.ts +23 -0
  188. package/dist/leader/diffout.js +8 -1
  189. package/dist/leader/diffup.d.ts +32 -0
  190. package/dist/leader/diffup.js +43 -0
  191. package/dist/leader/endpoint.d.ts +20 -0
  192. package/dist/leader/endpoint.js +25 -2
  193. package/dist/leader/fanout.d.ts +60 -0
  194. package/dist/leader/fanout.js +29 -3
  195. package/dist/leader/grader-env-factory.d.ts +74 -0
  196. package/dist/leader/grader-env-factory.js +62 -3
  197. package/dist/leader/leader.d.ts +97 -0
  198. package/dist/leader/leader.js +151 -4
  199. package/dist/leader/merge.d.ts +94 -0
  200. package/dist/leader/merge.js +112 -9
  201. package/dist/leader/planner.d.ts +52 -0
  202. package/dist/leader/planner.js +47 -3
  203. package/dist/leader/repair-oracle.d.ts +61 -0
  204. package/dist/leader/repair-oracle.js +59 -1
  205. package/dist/leader/repair-wire.d.ts +78 -0
  206. package/dist/leader/repair-wire.js +79 -2
  207. package/dist/leader/wire.d.ts +79 -0
  208. package/dist/leader/wire.js +255 -8
  209. package/dist/lsp/e2b-bridge.d.ts +16 -0
  210. package/dist/lsp/e2b-bridge.js +32 -4
  211. package/dist/lsp/e2b-manager.d.ts +10 -0
  212. package/dist/lsp/e2b-manager.js +18 -2
  213. package/dist/lsp/lsp-frames.d.ts +12 -0
  214. package/dist/lsp/lsp-frames.js +12 -0
  215. package/dist/lsp/manager.d.ts +12 -0
  216. package/dist/lsp/manager.js +35 -2
  217. package/dist/lsp/types.d.ts +10 -0
  218. package/dist/lsp/ws-transport.d.ts +12 -0
  219. package/dist/lsp/ws-transport.js +53 -5
  220. package/dist/lsp-evict.d.ts +14 -0
  221. package/dist/lsp-evict.js +15 -1
  222. package/dist/main.js +2132 -96
  223. package/dist/memory-export.d.ts +1 -0
  224. package/dist/memory-export.js +4 -0
  225. package/dist/memory-scope.d.ts +49 -0
  226. package/dist/memory-scope.js +87 -4
  227. package/dist/memory-sync-client.d.ts +24 -0
  228. package/dist/memory-sync-client.js +41 -2
  229. package/dist/memory-sync.d.ts +23 -0
  230. package/dist/memory-sync.js +70 -1
  231. package/dist/model-select.d.ts +39 -0
  232. package/dist/model-select.js +42 -1
  233. package/dist/observability/cost-quota.d.ts +20 -0
  234. package/dist/observability/cost-quota.js +4 -0
  235. package/dist/observability/cost-taxonomy.d.ts +45 -0
  236. package/dist/observability/cost-taxonomy.js +34 -0
  237. package/dist/observability/logger.d.ts +1 -0
  238. package/dist/observability/logger.js +6 -0
  239. package/dist/observability/metrics.d.ts +13 -0
  240. package/dist/observability/metrics.js +63 -0
  241. package/dist/observability/otel-exporter.d.ts +9 -0
  242. package/dist/observability/otel-exporter.js +11 -3
  243. package/dist/observability/principal-context.d.ts +9 -0
  244. package/dist/observability/principal-context.js +9 -0
  245. package/dist/observability/prompt-manifest.d.ts +37 -0
  246. package/dist/observability/prompt-manifest.js +35 -1
  247. package/dist/observability/rate-limit.d.ts +10 -0
  248. package/dist/observability/rate-limit.js +1 -0
  249. package/dist/observability/tool-trace.d.ts +24 -0
  250. package/dist/observability/tool-trace.js +41 -1
  251. package/dist/orchestration/hardened-vm-runner.d.ts +107 -0
  252. package/dist/orchestration/hardened-vm-runner.js +119 -4
  253. package/dist/orchestration/hardened-vm-worker-runner.d.ts +14 -0
  254. package/dist/orchestration/hardened-vm-worker-runner.js +26 -1
  255. package/dist/orchestration/hardened-vm-worker.js +27 -0
  256. package/dist/orchestration/subagent-steer.d.ts +44 -0
  257. package/dist/orchestration/subagent-steer.js +45 -1
  258. package/dist/orchestration/workflow-agent-steer.d.ts +54 -0
  259. package/dist/orchestration/workflow-agent-steer.js +80 -1
  260. package/dist/orchestration/workflow-completion-inbox.d.ts +189 -2
  261. package/dist/orchestration/workflow-completion-inbox.js +283 -32
  262. package/dist/orchestration/workflow-notify-journal.d.ts +132 -1
  263. package/dist/orchestration/workflow-notify-journal.js +187 -13
  264. package/dist/parked-decide.d.ts +61 -0
  265. package/dist/parked-decide.js +58 -1
  266. package/dist/per-task-image.d.ts +54 -0
  267. package/dist/per-task-image.js +41 -0
  268. package/dist/plan-cache-probe.d.ts +35 -0
  269. package/dist/plan-cache-probe.js +18 -3
  270. package/dist/plugins/approval-exemption-store.d.ts +7 -0
  271. package/dist/plugins/approval-exemption-store.js +28 -2
  272. package/dist/plugins/approval-store-sql.d.ts +64 -0
  273. package/dist/plugins/approval-store-sql.js +32 -0
  274. package/dist/plugins/background-agent-store-sql.d.ts +55 -0
  275. package/dist/plugins/background-agent-store-sql.js +91 -4
  276. package/dist/plugins/background-shell-support.d.ts +98 -0
  277. package/dist/plugins/background-shell-support.js +117 -13
  278. package/dist/plugins/blob-backend.d.ts +104 -0
  279. package/dist/plugins/blob-backend.js +163 -6
  280. package/dist/plugins/breaker-state-sql.d.ts +65 -1
  281. package/dist/plugins/breaker-state-sql.js +46 -8
  282. package/dist/plugins/caching-session-store.d.ts +42 -0
  283. package/dist/plugins/caching-session-store.js +63 -3
  284. package/dist/plugins/checkpoint-store-sql.d.ts +179 -0
  285. package/dist/plugins/checkpoint-store-sql.js +239 -11
  286. package/dist/plugins/file-outcome-sink.d.ts +4 -0
  287. package/dist/plugins/file-outcome-sink.js +9 -0
  288. package/dist/plugins/file-resume-anchor-store.d.ts +20 -0
  289. package/dist/plugins/file-resume-anchor-store.js +43 -4
  290. package/dist/plugins/file-run-store.d.ts +90 -0
  291. package/dist/plugins/file-run-store.js +145 -13
  292. package/dist/plugins/file-snapshot-store-sql.d.ts +133 -1
  293. package/dist/plugins/file-snapshot-store-sql.js +178 -10
  294. package/dist/plugins/file-workflow-journal-store.d.ts +10 -0
  295. package/dist/plugins/file-workflow-journal-store.js +10 -0
  296. package/dist/plugins/fork-routing-session-store.d.ts +47 -0
  297. package/dist/plugins/fork-routing-session-store.js +51 -3
  298. package/dist/plugins/host-platform.d.ts +69 -0
  299. package/dist/plugins/host-platform.js +81 -2
  300. package/dist/plugins/image-bake-store-sql.d.ts +136 -1
  301. package/dist/plugins/image-bake-store-sql.js +241 -7
  302. package/dist/plugins/image-index-sql.d.ts +46 -0
  303. package/dist/plugins/image-index-sql.js +113 -4
  304. package/dist/plugins/k8s-bg-scripts.d.ts +34 -0
  305. package/dist/plugins/k8s-bg-scripts.js +88 -4
  306. package/dist/plugins/k8s-exec-protocol.d.ts +33 -0
  307. package/dist/plugins/k8s-exec-protocol.js +41 -0
  308. package/dist/plugins/local-checkpoint-store.d.ts +40 -0
  309. package/dist/plugins/local-checkpoint-store.js +86 -7
  310. package/dist/plugins/local-session-store.d.ts +157 -0
  311. package/dist/plugins/local-session-store.js +289 -34
  312. package/dist/plugins/local-task-attachment-store.js +14 -2
  313. package/dist/plugins/mailbox-store-sql.d.ts +38 -0
  314. package/dist/plugins/mailbox-store-sql.js +39 -6
  315. package/dist/plugins/memory-engine-pg.d.ts +30 -0
  316. package/dist/plugins/memory-engine-pg.js +122 -4
  317. package/dist/plugins/memory-engine-tidb.d.ts +26 -0
  318. package/dist/plugins/memory-engine-tidb.js +115 -5
  319. package/dist/plugins/memory-engine-vector-util.d.ts +5 -0
  320. package/dist/plugins/memory-engine-vector-util.js +5 -0
  321. package/dist/plugins/memory-resume-anchor-store.d.ts +8 -0
  322. package/dist/plugins/memory-resume-anchor-store.js +17 -0
  323. package/dist/plugins/memory-run-store.d.ts +63 -0
  324. package/dist/plugins/memory-run-store.js +73 -8
  325. package/dist/plugins/memory-session-policy-store.d.ts +17 -0
  326. package/dist/plugins/memory-session-policy-store.js +17 -0
  327. package/dist/plugins/memory-sync-store-pg.d.ts +42 -0
  328. package/dist/plugins/memory-sync-store-pg.js +45 -2
  329. package/dist/plugins/memory-sync-store-tidb.d.ts +9 -0
  330. package/dist/plugins/memory-sync-store-tidb.js +31 -1
  331. package/dist/plugins/outcome-ledger-sql.d.ts +63 -0
  332. package/dist/plugins/outcome-ledger-sql.js +97 -3
  333. package/dist/plugins/pg-approval-store.d.ts +7 -0
  334. package/dist/plugins/pg-approval-store.js +7 -0
  335. package/dist/plugins/pg-breaker-state.d.ts +6 -0
  336. package/dist/plugins/pg-breaker-state.js +6 -0
  337. package/dist/plugins/pg-checkpoint-store.d.ts +8 -0
  338. package/dist/plugins/pg-checkpoint-store.js +8 -0
  339. package/dist/plugins/pg-cost-quota.d.ts +17 -0
  340. package/dist/plugins/pg-cost-quota.js +6 -0
  341. package/dist/plugins/pg-file-snapshot-store.d.ts +6 -0
  342. package/dist/plugins/pg-file-snapshot-store.js +6 -0
  343. package/dist/plugins/pg-image-bake.d.ts +9 -0
  344. package/dist/plugins/pg-image-bake.js +9 -0
  345. package/dist/plugins/pg-image-index.d.ts +9 -0
  346. package/dist/plugins/pg-image-index.js +9 -0
  347. package/dist/plugins/pg-outcome-ledger.d.ts +9 -0
  348. package/dist/plugins/pg-outcome-ledger.js +9 -0
  349. package/dist/plugins/pg-pool.d.ts +50 -0
  350. package/dist/plugins/pg-pool.js +78 -0
  351. package/dist/plugins/pg-query.d.ts +11 -0
  352. package/dist/plugins/pg-rate-limiter.d.ts +23 -0
  353. package/dist/plugins/pg-rate-limiter.js +12 -2
  354. package/dist/plugins/pg-resume-anchor-store.d.ts +5 -0
  355. package/dist/plugins/pg-resume-anchor-store.js +5 -0
  356. package/dist/plugins/pg-run-store.d.ts +7 -0
  357. package/dist/plugins/pg-run-store.js +7 -0
  358. package/dist/plugins/pg-safe-json.d.ts +13 -0
  359. package/dist/plugins/pg-safe-json.js +40 -4
  360. package/dist/plugins/pg-session-policy-store.d.ts +5 -0
  361. package/dist/plugins/pg-session-policy-store.js +5 -0
  362. package/dist/plugins/pg-session-storage.d.ts +72 -0
  363. package/dist/plugins/pg-session-storage.js +152 -20
  364. package/dist/plugins/pg-session-store.d.ts +10 -0
  365. package/dist/plugins/pg-session-store.js +10 -0
  366. package/dist/plugins/pg-tool-result-store.d.ts +7 -0
  367. package/dist/plugins/pg-tool-result-store.js +7 -0
  368. package/dist/plugins/pg-workflow-journal-store.d.ts +7 -0
  369. package/dist/plugins/pg-workflow-journal-store.js +7 -0
  370. package/dist/plugins/pg-workflow-run-store.d.ts +7 -0
  371. package/dist/plugins/pg-workflow-run-store.js +7 -0
  372. package/dist/plugins/posix-shell-fs.d.ts +3 -0
  373. package/dist/plugins/posix-shell-fs.js +31 -1
  374. package/dist/plugins/remote-env-adb.d.ts +25 -0
  375. package/dist/plugins/remote-env-adb.js +95 -12
  376. package/dist/plugins/remote-env-e2b.d.ts +146 -0
  377. package/dist/plugins/remote-env-e2b.js +371 -40
  378. package/dist/plugins/remote-env-file-error.d.ts +34 -0
  379. package/dist/plugins/remote-env-file-error.js +37 -0
  380. package/dist/plugins/remote-env-host.d.ts +151 -0
  381. package/dist/plugins/remote-env-host.js +468 -70
  382. package/dist/plugins/remote-env-k8s.d.ts +135 -0
  383. package/dist/plugins/remote-env-k8s.js +318 -32
  384. package/dist/plugins/remote-env-local-docker.d.ts +65 -0
  385. package/dist/plugins/remote-env-local-docker.js +149 -24
  386. package/dist/plugins/remote-env-ssh.d.ts +35 -0
  387. package/dist/plugins/remote-env-ssh.js +117 -20
  388. package/dist/plugins/remote-scratchpad.d.ts +38 -0
  389. package/dist/plugins/remote-scratchpad.js +32 -2
  390. package/dist/plugins/remote-shell.d.ts +22 -0
  391. package/dist/plugins/remote-shell.js +27 -1
  392. package/dist/plugins/resume-anchor-store-sql.d.ts +28 -0
  393. package/dist/plugins/resume-anchor-store-sql.js +11 -0
  394. package/dist/plugins/roster-store-sql.d.ts +27 -0
  395. package/dist/plugins/roster-store-sql.js +54 -0
  396. package/dist/plugins/run-store-sql.d.ts +194 -0
  397. package/dist/plugins/run-store-sql.js +210 -9
  398. package/dist/plugins/s3-presign.d.ts +20 -0
  399. package/dist/plugins/s3-presign.js +49 -2
  400. package/dist/plugins/scheduler-support.d.ts +12 -0
  401. package/dist/plugins/scheduler-support.js +75 -3
  402. package/dist/plugins/send-file-ledger.d.ts +15 -0
  403. package/dist/plugins/send-file-ledger.js +53 -4
  404. package/dist/plugins/send-user-file.d.ts +77 -0
  405. package/dist/plugins/send-user-file.js +92 -4
  406. package/dist/plugins/session-policy-store-sql.d.ts +15 -0
  407. package/dist/plugins/session-policy-store-sql.js +75 -3
  408. package/dist/plugins/session-store.d.ts +45 -0
  409. package/dist/plugins/session-store.js +51 -0
  410. package/dist/plugins/sql-driver.d.ts +77 -0
  411. package/dist/plugins/sql-driver.js +4 -0
  412. package/dist/plugins/sql-escape.d.ts +8 -0
  413. package/dist/plugins/sql-escape.js +8 -0
  414. package/dist/plugins/sql-row-helpers.d.ts +25 -0
  415. package/dist/plugins/sql-row-helpers.js +25 -0
  416. package/dist/plugins/store-backend.d.ts +103 -0
  417. package/dist/plugins/store-backend.js +120 -27
  418. package/dist/plugins/store-contracts.d.ts +62 -0
  419. package/dist/plugins/store-contracts.js +3 -0
  420. package/dist/plugins/task-attachment-store.d.ts +60 -0
  421. package/dist/plugins/task-attachment-store.js +49 -3
  422. package/dist/plugins/task-list-store-sql.d.ts +25 -0
  423. package/dist/plugins/task-list-store-sql.js +21 -0
  424. package/dist/plugins/tidb-approval-store.d.ts +6 -0
  425. package/dist/plugins/tidb-approval-store.js +6 -0
  426. package/dist/plugins/tidb-breaker-state.d.ts +5 -0
  427. package/dist/plugins/tidb-breaker-state.js +5 -0
  428. package/dist/plugins/tidb-checkpoint-store.d.ts +7 -0
  429. package/dist/plugins/tidb-checkpoint-store.js +7 -0
  430. package/dist/plugins/tidb-cost-quota.d.ts +6 -0
  431. package/dist/plugins/tidb-cost-quota.js +1 -0
  432. package/dist/plugins/tidb-file-snapshot-store.d.ts +6 -0
  433. package/dist/plugins/tidb-file-snapshot-store.js +6 -0
  434. package/dist/plugins/tidb-image-bake.d.ts +8 -0
  435. package/dist/plugins/tidb-image-bake.js +8 -0
  436. package/dist/plugins/tidb-image-index.d.ts +8 -0
  437. package/dist/plugins/tidb-image-index.js +8 -0
  438. package/dist/plugins/tidb-outcome-ledger.d.ts +10 -0
  439. package/dist/plugins/tidb-outcome-ledger.js +10 -0
  440. package/dist/plugins/tidb-pool.d.ts +56 -0
  441. package/dist/plugins/tidb-pool.js +181 -1
  442. package/dist/plugins/tidb-rate-limiter.d.ts +11 -0
  443. package/dist/plugins/tidb-rate-limiter.js +5 -2
  444. package/dist/plugins/tidb-resume-anchor-store.d.ts +5 -0
  445. package/dist/plugins/tidb-resume-anchor-store.js +5 -0
  446. package/dist/plugins/tidb-run-store.d.ts +7 -0
  447. package/dist/plugins/tidb-run-store.js +7 -0
  448. package/dist/plugins/tidb-session-policy-store.d.ts +5 -0
  449. package/dist/plugins/tidb-session-policy-store.js +5 -0
  450. package/dist/plugins/tidb-session-storage.d.ts +43 -0
  451. package/dist/plugins/tidb-session-storage.js +62 -3
  452. package/dist/plugins/tidb-session-store.d.ts +142 -0
  453. package/dist/plugins/tidb-session-store.js +264 -16
  454. package/dist/plugins/tidb-tool-result-store.d.ts +5 -0
  455. package/dist/plugins/tidb-tool-result-store.js +7 -0
  456. package/dist/plugins/tidb-workflow-journal-store.d.ts +7 -0
  457. package/dist/plugins/tidb-workflow-journal-store.js +7 -0
  458. package/dist/plugins/tidb-workflow-run-store.d.ts +8 -0
  459. package/dist/plugins/tidb-workflow-run-store.js +8 -0
  460. package/dist/plugins/tool-result-store-sql.d.ts +58 -0
  461. package/dist/plugins/tool-result-store-sql.js +38 -2
  462. package/dist/plugins/web-search.d.ts +35 -0
  463. package/dist/plugins/web-search.js +45 -9
  464. package/dist/plugins/workflow-journal-limits.d.ts +10 -0
  465. package/dist/plugins/workflow-journal-limits.js +10 -0
  466. package/dist/plugins/workflow-journal-store-sql.d.ts +78 -0
  467. package/dist/plugins/workflow-journal-store-sql.js +58 -2
  468. package/dist/plugins/workflow-run-store-sql.d.ts +84 -0
  469. package/dist/plugins/workflow-run-store-sql.js +84 -6
  470. package/dist/plugins/worktree-isolation.d.ts +45 -0
  471. package/dist/plugins/worktree-isolation.js +105 -6
  472. package/dist/plugins/write-behind-counter.d.ts +56 -1
  473. package/dist/plugins/write-behind-counter.js +53 -14
  474. package/dist/principal-jwt.d.ts +24 -0
  475. package/dist/principal-jwt.js +60 -5
  476. package/dist/project-memory.d.ts +22 -0
  477. package/dist/project-memory.js +144 -15
  478. package/dist/prompts-domain-validate.d.ts +28 -0
  479. package/dist/prompts-domain-validate.js +42 -1
  480. package/dist/question.d.ts +33 -0
  481. package/dist/question.js +69 -2
  482. package/dist/resource-suspend.d.ts +21 -0
  483. package/dist/resource-suspend.js +16 -0
  484. package/dist/router/route-orchestration.d.ts +78 -0
  485. package/dist/router/route-orchestration.js +77 -0
  486. package/dist/run-local.d.ts +32 -0
  487. package/dist/run-local.js +172 -8
  488. package/dist/runs.d.ts +189 -4
  489. package/dist/runs.js +465 -24
  490. package/dist/runtime-caps-resolver.d.ts +106 -0
  491. package/dist/runtime-caps-resolver.js +89 -4
  492. package/dist/runtime-governance.d.ts +84 -0
  493. package/dist/runtime-governance.js +127 -1
  494. package/dist/sandbox-pkg-source.d.ts +23 -0
  495. package/dist/sandbox-pkg-source.js +37 -0
  496. package/dist/sealed-key.d.ts +54 -0
  497. package/dist/sealed-key.js +68 -3
  498. package/dist/security.d.ts +108 -0
  499. package/dist/security.js +145 -6
  500. package/dist/sema-registry.d.ts +21 -0
  501. package/dist/sema-registry.js +21 -0
  502. package/dist/session-leaf-bus.d.ts +31 -0
  503. package/dist/session-leaf-bus.js +32 -0
  504. package/dist/session-sync-content.d.ts +46 -0
  505. package/dist/session-sync-content.js +47 -1
  506. package/dist/session-sync-kernel.d.ts +89 -0
  507. package/dist/session-sync-kernel.js +48 -3
  508. package/dist/session-sync.d.ts +125 -0
  509. package/dist/session-sync.js +181 -8
  510. package/dist/session-titler.d.ts +26 -0
  511. package/dist/session-titler.js +41 -7
  512. package/dist/session-watch.d.ts +53 -1
  513. package/dist/session-watch.js +73 -9
  514. package/dist/sighup-idle.d.ts +30 -0
  515. package/dist/sighup-idle.js +12 -1
  516. package/dist/spec-fields.d.ts +91 -3
  517. package/dist/spec-fields.js +116 -4
  518. package/dist/task-cwd.d.ts +50 -0
  519. package/dist/task-cwd.js +68 -1
  520. package/dist/task-mcp.d.ts +47 -0
  521. package/dist/task-mcp.js +29 -0
  522. package/dist/task-settings.d.ts +175 -0
  523. package/dist/task-settings.js +226 -11
  524. package/dist/task-workflow.d.ts +67 -0
  525. package/dist/task-workflow.js +75 -8
  526. package/dist/tool-approval.d.ts +91 -0
  527. package/dist/tool-approval.js +215 -0
  528. package/dist/trace/artifacts.d.ts +9 -0
  529. package/dist/trace/artifacts.js +30 -5
  530. package/dist/trace/core-keyset-guard.d.ts +18 -0
  531. package/dist/trace/ledger-sink.d.ts +43 -0
  532. package/dist/trace/ledger-sink.js +64 -5
  533. package/dist/trace/project.d.ts +133 -0
  534. package/dist/trace/project.js +189 -2
  535. package/dist/trace/redact.d.ts +19 -0
  536. package/dist/trace/redact.js +66 -9
  537. package/dist/usage-analytics.d.ts +26 -0
  538. package/dist/usage-analytics.js +26 -3
  539. package/package.json +1 -1
@@ -1,3 +1,35 @@
1
+ /**
2
+ * `RemoteK8sExecutionEnv` — the Kubernetes pod-sandbox implementation of core's `RemoteExecutionEnv` seam
3
+ * (design/61). A PEER adapter to E2B/SSH/ADB (NOT chained). Use case: self-hosted sandboxes on our k3s with
4
+ * **Kata Containers** (`runtimeClassName: kata-qemu`) — a real VM boundary (guest kernel ≠ host, proven in the
5
+ * 2026-06-10 PoC) at ~2s startup, par with managed E2B.
6
+ *
7
+ * 🔴 Capabilities: `{ isolation: true, suspendable: false }` — Kata gives VM isolation, but
8
+ * there is NO managed memory snapshot (E2B-style pause/resume). `suspendVM`/`resumeVM`/`postResumeInit` →
9
+ * typed `"unsupported"` so durable suspend degrades honestly (checkpoint + cold rebuild, in-VM process state
10
+ * like a language server is lost). True memory snapshots = a separate CRIU/Kata-snapshot project, not implied.
11
+ *
12
+ * Transport: the k8s REST API + the exec WebSocket (channel-multiplexed: 1-byte channel prefix per frame —
13
+ * 1=stdout 2=stderr 3=status JSON) spoken directly over `ws` — deliberately NO `@kubernetes/client-node`
14
+ * dependency (its exec path is the only thing we'd use; our liveness/timeout discipline plugs straight into a
15
+ * raw socket, and the fault-injection seam stays trivial). Unlike `adb exec-out`, the status channel carries a
16
+ * REAL exit code.
17
+ *
18
+ * 🔴 Subprotocol = `v5.channel.k8s.io` first, `v4` fallback (night-drill 2026-06-11): the apiserver's **v4**
19
+ * WebSocket exec writer STALLS after the first ~6KB of a fast output burst (verified: the TCP socket stays
20
+ * healthy/flowing, the apiserver just stops sending; v5 streams the same 1MB+ burst to completion). v5 (k8s
21
+ * 1.29+) is what modern kubectl negotiates; the frame format is identical (it only adds a close-channel we
22
+ * ignore), so offering both lets a v5 apiserver fix the stall while a v4-only one still works for small output.
23
+ *
24
+ * Timeouts follow the E2B class-fix taxonomy: control-plane RPCs get a short wall-clock
25
+ * (`rpcTimeoutMs`); commands get a zero-progress LIVENESS bound (`livenessMs`) plus the caller's explicit
26
+ * wall-clock; pod readiness gets its own generous bound (image pull can be slow). ⚠️ killing the exec socket
27
+ * does NOT kill the remote process — acceptable here because the pod is DISPOSABLE (destroyed with the task;
28
+ * `activeDeadlineSeconds` is the leak guard), unlike the SSH real-machine posture.
29
+ *
30
+ * Auth: a bearer token + CA, either explicit (`K8S_API_URL`/`K8S_TOKEN`/`K8S_CA_CERT_B64`) or in-cluster
31
+ * (service-account files + `KUBERNETES_SERVICE_HOST`) — control-plane config, never agent-reachable.
32
+ */
1
33
  import path from "node:path";
2
34
  import { readFileSync } from "node:fs";
3
35
  import https from "node:https";
@@ -12,13 +44,25 @@ import { createPosixShellFs } from "./posix-shell-fs.js";
12
44
  import { randomUUID } from "node:crypto";
13
45
  import { BackgroundShellManager, seedMemStream, feedMemStream, drainMemStream } from "./background-shell-support.js";
14
46
  const PROVIDER = "k8s";
47
+ /**
48
+ * design/103 background-shell (TOB, path ① — pod-side file buffer re-read by byte cursor). Bounds for kata/k8s.
49
+ * A background job cannot outlive its pod (`activeDeadlineSeconds`), so `maxBgTimeoutSec` is additionally clamped
50
+ * to the pod lifetime at construction. `maxConcurrent` mirrors the TOC default (8) — anti fork-bomb.
51
+ */
15
52
  const K8S_BG_DIR_ROOT = "/tmp/.sema-bg";
16
53
  const K8S_BG_MAX_CONCURRENT = numEnvOr("K8S_BG_MAX_CONCURRENT", 8, 1);
17
54
  const K8S_BG_DEFAULT_TIMEOUT_SEC = numEnvOr("K8S_BG_DEFAULT_TIMEOUT_SEC", 300, 1);
18
55
  const K8S_BG_MAX_TIMEOUT_SEC = numEnvOr("K8S_BG_MAX_TIMEOUT_SEC", 1800, K8S_BG_DEFAULT_TIMEOUT_SEC);
56
+ /** Max bytes returned per stream per poll (bounds the control-plane transfer; the model only displays ~20K anyway). */
19
57
  const K8S_BG_READ_CAP = numEnvOr("K8S_BG_READ_CAP", 1024 * 1024, 64 * 1024);
58
+ /** Per-stream pod-side file cap (FIFO+`head` — a runaway `yes` is SIGPIPE'd at this bound; the pod has no ephemeral-storage limit). */
20
59
  const K8S_BG_FILE_CAP = numEnvOr("K8S_BG_FILE_CAP", 64 * 1024 * 1024, K8S_BG_READ_CAP);
60
+ /** Per-stream control-plane drain cap for an ADOPTED (mid-flight detach) shell — its output rides the still-open
61
+ * exec WebSocket into worker memory, not pod files, so bound the un-polled queue (drop-oldest → `truncated`). */
21
62
  const K8S_BG_MEM_CAP = numEnvOr("K8S_BG_MEM_CAP", 8 * 1024 * 1024, 64 * 1024);
63
+ // design/158 A13: the pure background-shell pod-side script generation/parsing group (buildBg*Script ×5 +
64
+ // buildDetachCapableExec + parseBgPollOutput + ParsedBgPoll) moved VERBATIM to ./k8s-bg-scripts.js —
65
+ // value-imported below (the class body still calls them) and re-exported so existing importers keep working.
22
66
  import { buildBgRunnerScript, buildBgLauncherScript, buildBgPollScript, buildBgKillScript, buildBgDisposeScript, buildDetachCapableExec, parseBgPollOutput, } from "./k8s-bg-scripts.js";
23
67
  export { buildBgRunnerScript, buildBgLauncherScript, buildBgPollScript, buildBgKillScript, buildBgDisposeScript, buildDetachCapableExec, parseBgPollOutput, } from "./k8s-bg-scripts.js";
24
68
  const ok = (value) => ({ ok: true, value });
@@ -27,12 +71,29 @@ const unsupported = (op) => ({
27
71
  error: new RemoteExecutionError("unsupported", `${op} is not supported on a Kata pod sandbox (no managed memory snapshot — capabilities.suspendable=false; durable suspend degrades to checkpoint + cold rebuild)`),
28
72
  });
29
73
  const SA_DIR = "/var/run/secrets/kubernetes.io/serviceaccount";
74
+ /** Exec WebSocket subprotocols, preference order (see header — v4 burst-stall). */
30
75
  const EXEC_SUBPROTOCOLS = ["v5.channel.k8s.io", "v4.channel.k8s.io"];
76
+ /**
77
+ * argv-embedded base64 write chunk (SOURCE bytes). 🔴 The bound is Linux `MAX_ARG_STRLEN` = 128KiB (131072),
78
+ * the limit on a SINGLE argv string — and the whole `printf '%s' '<base64>' | base64 -d >> '<path>'` command
79
+ * is passed as one `sh -c <cmd>` argument. base64 inflates 4/3, so a 96KiB source chunk made a 131072-byte
80
+ * base64 = exactly the limit ⇒ silent truncation (night-drill: writes >95KiB failed "command failed"). 64KiB
81
+ * source → 87KiB base64 + wrapper ≈ 87KiB command, a safe ~44KiB under the limit.
82
+ */
31
83
  const WRITE_CHUNK_BYTES = 64 * 1024;
84
+ // design/158 A13: the pod-spec patch group (PodSpecPatch + applyPodSpecPatch + its hardening/volume guards)
85
+ // moved VERBATIM to ./k8s-exec-protocol.js — value-imported below (the class body calls applyPodSpecPatch)
86
+ // and re-exported so existing importers keep working.
32
87
  import { applyPodSpecPatch } from "./k8s-exec-protocol.js";
33
88
  export { applyPodSpecPatch } from "./k8s-exec-protocol.js";
89
+ // design/158 A13: execSocketTlsOptions + exitCodeFromStatus moved VERBATIM to ./k8s-exec-protocol.js
90
+ // (errorMessageOf, sandwiched between them in the original layout, stays HERE — it is a small ws-error
91
+ // helper, not part of the pod-spec-patch/protocol group). Value-imported below (the class body calls both)
92
+ // and re-exported so existing importers keep working.
34
93
  import { execSocketTlsOptions, exitCodeFromStatus } from "./k8s-exec-protocol.js";
35
94
  export { execSocketTlsOptions } from "./k8s-exec-protocol.js";
95
+ /** `Error`-or-event → message. Bun's ws shim emits browser-style ErrorEvents (NOT instanceof Error) whose
96
+ * String() is "[object ErrorEvent]" — that masked the real TLS failure for a whole drill. */
36
97
  export function errorMessageOf(e) {
37
98
  if (e instanceof Error)
38
99
  return e.message;
@@ -44,10 +105,15 @@ export class RemoteK8sExecutionEnv {
44
105
  capabilities;
45
106
  cwd;
46
107
  podName;
108
+ /** Cluster-internal pod address, captured when the pod reaches Running — the LSP `getHost` surface.
109
+ * Lifecycle mirrors podName exactly (set on connect, cleared on suspend/destroy/dead-reconnect). */
47
110
  podIP;
48
111
  destroyed = false;
112
+ /** Set after a successful resumeVM so workspaceHandle() carries the snapshot it was restored from. */
49
113
  handleSnapshotId;
114
+ /** In-flight exec count — `suspendVM` refuses while a command runs (council #4 suspend×in-flight). */
50
115
  inFlightExecs = 0;
116
+ /** Memoized in-flight connect (lazy-connect; prevents concurrent double pod-create — the E2B race lesson). */
51
117
  connectPromise;
52
118
  cfg;
53
119
  deps;
@@ -72,17 +138,24 @@ export class RemoteK8sExecutionEnv {
72
138
  ...(config.s3Snapshot ? { s3Snapshot: config.s3Snapshot } : {}),
73
139
  ...(config.sessionId ? { sessionId: config.sessionId } : {}),
74
140
  ...(config.podSpecPatch ? { podSpecPatch: config.podSpecPatch } : {}),
75
- ...(config.podEnv ? { podEnv: config.podEnv } : {}),
141
+ ...(config.podEnv ? { podEnv: config.podEnv } : {}), // RFC B5: region package-source env
76
142
  };
77
143
  this.cwd = this.cfg.mountPath;
78
144
  this.deps = deps ?? {};
145
+ // Workspace snapshots make this env suspendable (files durable across suspend/resume); without an S3
146
+ // store it stays the honest checkpoint + cold-rebuild degrade.
79
147
  this.capabilities = { isolation: true, suspendable: !!this.cfg.s3Snapshot };
148
+ // Clamp BOTH the ceiling AND the default to the pod lifetime — a short-lived pod must not advertise a default
149
+ // bigger than its max (the manager would clamp the actual spawn, making the advertised default a lie).
80
150
  const bgMax = Math.max(1, Math.min(K8S_BG_MAX_TIMEOUT_SEC, Math.floor(this.cfg.timeoutMs / 1000)));
81
151
  this.backgroundCapabilities = {
82
152
  supported: true,
83
153
  maxConcurrent: K8S_BG_MAX_CONCURRENT,
84
154
  defaultBgTimeoutSec: Math.min(K8S_BG_DEFAULT_TIMEOUT_SEC, bgMax),
85
155
  maxBgTimeoutSec: bgMax,
156
+ // core 1.207 design/116 detach (1.82): a running foreground exec is ADOPTED as a background shell
157
+ // (mid-flight ctrl+b) — the exec WebSocket keeps streaming into memory-mode drain buffers; kill works
158
+ // through the pgid the detach-capable exec prelude self-reported (buildDetachCapableExec).
86
159
  supportsDetach: true,
87
160
  };
88
161
  }
@@ -92,6 +165,8 @@ export class RemoteK8sExecutionEnv {
92
165
  workspaceHandle() {
93
166
  return { sandboxId: this.podName ?? "(unconnected)", provider: PROVIDER, mountPath: this.cwd, ...(this.handleSnapshotId ? { snapshotId: this.handleSnapshotId } : {}) };
94
167
  }
168
+ // ─────────────────────────────── auth / transport ───────────────────────────────
169
+ /** Resolved lazily so an in-cluster pod picks up its service account without explicit config. */
95
170
  creds() {
96
171
  const apiUrl = this.cfg.apiUrl ??
97
172
  (process.env.KUBERNETES_SERVICE_HOST ? `https://${process.env.KUBERNETES_SERVICE_HOST}:${process.env.KUBERNETES_SERVICE_PORT ?? "443"}` : undefined);
@@ -117,6 +192,7 @@ export class RemoteK8sExecutionEnv {
117
192
  }
118
193
  return { apiUrl: apiUrl.replace(/\/+$/, ""), token, ca };
119
194
  }
195
+ /** Control-plane REST call (pod create/get/delete), bounded by rpcTimeoutMs. Injectable for tests. */
120
196
  async request(method, apiPath, body, opts) {
121
197
  if (this.deps.request)
122
198
  return this.deps.request(method, apiPath, body, opts);
@@ -171,15 +247,22 @@ export class RemoteK8sExecutionEnv {
171
247
  ...(url.startsWith("wss:") ? execSocketTlsOptions(ca, this.cfg.insecureTls) : {}),
172
248
  });
173
249
  }
250
+ // ─────────────────────────────── lifecycle ───────────────────────────────
174
251
  async connect(config) {
175
252
  const policy = {
176
- retryableCodes: ["connect_failed", "timeout", "transport_lost"],
253
+ retryableCodes: ["connect_failed", "timeout", "transport_lost"], // NOT auth_failed
177
254
  maxAttempts: this.cfg.retry.maxAttempts,
178
255
  backoffMs: (failed) => this.cfg.retry.backoffMs * failed,
179
256
  };
180
257
  const once = this.deps.connectOnce ?? ((s) => this.connectOnce(s));
181
258
  return withRetry(() => once(config?.abortSignal), policy, { signal: config?.abortSignal });
182
259
  }
260
+ /**
261
+ * Lazy-connect on first use — 🔴 core NEVER calls `connect()` (verified: the Runner only calls `resumeVM` on
262
+ * the resume path), so an adapter that demands an explicit connect can never work through the production
263
+ * hand-tool path (lazy-connect belongs in the adapter). Memoized so concurrent first ops share ONE
264
+ * pod create; a failed connect clears the memo (next op retries); `destroyed` is terminal.
265
+ */
183
266
  async ensureConnected() {
184
267
  if (this.podName)
185
268
  return ok(undefined);
@@ -188,7 +271,7 @@ export class RemoteK8sExecutionEnv {
188
271
  this.connectPromise ??= this.connect();
189
272
  const r = await this.connectPromise;
190
273
  if (!r.ok) {
191
- this.connectPromise = undefined;
274
+ this.connectPromise = undefined; // don't cache the failure — the next op gets a fresh attempt
192
275
  return { ok: false, error: new ExecutionError("shell_unavailable", `k8s sandbox connect failed: ${r.error.message}`, r.error) };
193
276
  }
194
277
  return ok(undefined);
@@ -209,16 +292,24 @@ export class RemoteK8sExecutionEnv {
209
292
  spec: {
210
293
  ...(this.cfg.runtimeClass ? { runtimeClassName: this.cfg.runtimeClass } : {}),
211
294
  restartPolicy: "Never",
295
+ // Defense-in-depth (council DESIGN#1): the agent runs INSIDE this pod; never mount a k8s SA token into
296
+ // it. The adapter holds the only credential it needs (control-plane side) — the workload gets none, so
297
+ // an accidental cluster-wide default-SA binding can't hand the agent the API.
212
298
  automountServiceAccountToken: false,
299
+ // Leak guard (the E2B-lifetime analogue): the pod self-terminates even if destroy() never runs.
213
300
  activeDeadlineSeconds: Math.ceil(this.cfg.timeoutMs / 1000),
214
301
  terminationGracePeriodSeconds: 1,
215
302
  containers: [
216
303
  {
217
304
  name: "sandbox",
218
305
  image: this.cfg.image,
306
+ // RFC B5: when a pkg-source profile is injected, run the image's materializer ONCE at pod start
307
+ // (root, before any exec — core's exec lanes are non-login `sh -c`, so the profile.d hook alone
308
+ // wouldn't fire there). Script-absent images no-op; podEnv unset ⇒ command byte-identical to today.
219
309
  command: this.cfg.podEnv?.SEMA_PKG_SOURCE
220
310
  ? ["sh", "-c", "[ -f /opt/sema/pkg-source.sh ] && sh /opt/sema/pkg-source.sh || true; sleep infinity"]
221
311
  : ["sh", "-c", "sleep infinity"],
312
+ // RFC B5: operator-trusted region env (pkg-source selection). Absent = no env key at all (byte-identical).
222
313
  ...(this.cfg.podEnv && Object.keys(this.cfg.podEnv).length > 0
223
314
  ? { env: Object.entries(this.cfg.podEnv).map(([name, value]) => ({ name, value: String(value) })) }
224
315
  : {}),
@@ -227,6 +318,9 @@ export class RemoteK8sExecutionEnv {
227
318
  ],
228
319
  },
229
320
  };
321
+ // 🔴 ADDITIVE deployment patch: add RO oracle mounts / hardened securityContext for the L8
322
+ // immutable-mount probe + the graderEnvFactory. Applied AFTER the hard invariants above so it can never weaken
323
+ // them (applyPodSpecPatch only touches volumes/mounts/securityContext/labels). No-op when unset (byte-identical).
230
324
  if (this.cfg.podSpecPatch)
231
325
  applyPodSpecPatch(pod, this.cfg.podSpecPatch);
232
326
  let created;
@@ -245,6 +339,7 @@ export class RemoteK8sExecutionEnv {
245
339
  const name = created.json?.metadata?.name;
246
340
  if (!name)
247
341
  return { ok: false, error: new RemoteExecutionError("connect_failed", "pod create returned no name") };
342
+ // Poll until Running (schedule + image pull + Kata VM boot), bounded by readyTimeoutMs.
248
343
  const deadline = Date.now() + this.cfg.readyTimeoutMs;
249
344
  let podIP;
250
345
  for (;;) {
@@ -281,12 +376,16 @@ export class RemoteK8sExecutionEnv {
281
376
  }
282
377
  await new Promise((r) => setTimeout(r, 1000));
283
378
  }
379
+ // Destroy-race guard (the E2B lazy-connect BLOCKER lesson): if destroy() ran while we were creating the
380
+ // pod, kill the fresh pod and bail — never resurrect a destroyed env (destroyed is terminal).
284
381
  if (this.destroyed) {
285
382
  await this.deletePod(name).catch(() => { });
286
383
  return { ok: false, error: new RemoteExecutionError("connect_failed", "env destroyed during connect") };
287
384
  }
288
385
  this.podName = name;
289
386
  this.podIP = podIP;
387
+ // Workspace root (the image may not ship it) — via runExec DIRECTLY: this.exec() would re-enter
388
+ // ensureConnected and deadlock on the still-pending connect memo. Failure = unusable sandbox.
290
389
  const mk = await this.runExec(`mkdir -p ${shellQuote(this.cfg.mountPath)}`);
291
390
  if (!mk.ok || mk.value.exitCode !== 0) {
292
391
  await this.deletePod(name).catch(() => { });
@@ -296,6 +395,14 @@ export class RemoteK8sExecutionEnv {
296
395
  }
297
396
  return ok(this.workspaceHandle());
298
397
  }
398
+ /**
399
+ * Build a validated, presigned URL for a snapshot object — `<prefix>/<sessionId>/<snapshotId>.tar.gz`.
400
+ * 🔴 sessionId comes from the client (`body.sessionId`, security.ts) and is NOT charset-validated upstream, so
401
+ * each path segment is allowlisted here (`^[A-Za-z0-9][A-Za-z0-9._-]*$`) before it enters the object key: a
402
+ * segment of `..` / `../x` / one containing `/` would otherwise let one tenant's snapshot key normalize into
403
+ * another tenant's prefix (cross-tenant read on resume / clobber on suspend). Returns a typed error instead of
404
+ * signing a traversing key. Folds the old snapshotKey()+s3Presign() pair so the two call sites can't diverge.
405
+ */
299
406
  presignSnapshot(method, snapshotId) {
300
407
  const s3 = this.cfg.s3Snapshot;
301
408
  const prefix = s3.keyPrefix ?? "kata";
@@ -315,29 +422,48 @@ export class RemoteK8sExecutionEnv {
315
422
  accessKey: s3.accessKey,
316
423
  secretKey: s3.secretKey,
317
424
  ...(s3.region ? { region: s3.region } : {}),
425
+ // Clamp the presigned-URL TTL to a 1h ceiling — a short-lived single-object credential must not become a
426
+ // long-lived bearer token via an over-large WORKSPACE_SNAPSHOT_TTL_SEC.
318
427
  expiresSec: Math.min(s3.presignTtlSec ?? 600, 3600),
319
428
  }),
320
429
  };
321
430
  }
431
+ /** Redact a presigned URL (and any lingering signature) from text before it enters an error/log — the URL is a
432
+ * short-lived bearer credential and must never reach a durable store (secret boundary). */
322
433
  redactUrl(text, url) {
323
434
  return text.split(url).join("<presigned-url>").replace(/X-Amz-Signature=[0-9a-f]+/gi, "X-Amz-Signature=<redacted>");
324
435
  }
436
+ /**
437
+ * Workspace-durable suspend: tar the workspace, upload to S3 via a short-lived adapter-presigned
438
+ * URL the pod curls (credentials never enter the sandbox), then delete the pod. The returned SnapshotId is
439
+ * opaque to core; we use a fresh id and persist the bytes under {@link snapshotKey}. NOT supported without an
440
+ * S3 store (returns the honest "unsupported" so durable suspend degrades to checkpoint + cold rebuild).
441
+ *
442
+ * 🔴 council #4 (suspend × in-flight exec): refuse while a command runs — tarring a mutating tree would
443
+ * snapshot a torn workspace. Core also won't suspend mid-tool, but the adapter enforces it independently.
444
+ */
325
445
  async suspendVM(_options) {
326
446
  if (!this.cfg.s3Snapshot)
327
447
  return unsupported("suspendVM");
328
448
  if (this.inFlightExecs > 0)
329
449
  return { ok: false, error: new RemoteExecutionError("command_in_flight", "cannot suspend while a command is in flight (would snapshot a torn workspace)") };
330
450
  const snapshotId = `kata-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
451
+ // A never-connected env (a plan-only task that suspended without touching the hand) has no workspace to
452
+ // capture — return a snapshotId with no object; resume tolerates the 404 as an empty workspace.
331
453
  if (!this.podName)
332
454
  return ok(snapshotId);
333
455
  const presigned = this.presignSnapshot("PUT", snapshotId);
334
456
  if (!presigned.ok)
335
457
  return presigned;
336
458
  const url = presigned.value;
459
+ // Materialize the archive to a pod-local temp file, THEN upload — a streamed `tar | curl -T -` sends
460
+ // Transfer-Encoding: chunked with no Content-Length, which MinIO rejects with HTTP 411 on a presigned PUT
461
+ // (live-verified). `curl -T <file>` sets Content-Length from the file size. Bytes still go pod→MinIO (never
462
+ // the worker); the temp file lives in the disposable pod and is removed regardless of curl's outcome.
337
463
  const tmp = "/tmp/ws-snap.tgz";
338
464
  const cmd = `tar czf ${tmp} -C ${shellQuote(this.cfg.mountPath)} . && curl -fsS -X PUT -T ${tmp} ${shellQuote(url)}; ` +
339
465
  `rc=$?; rm -f ${tmp}; exit $rc`;
340
- const r = await this.runExec(cmd, { timeoutMs: this.cfg.dataTimeoutMs, livenessMs: 0 });
466
+ const r = await this.runExec(cmd, { timeoutMs: this.cfg.dataTimeoutMs, livenessMs: 0 }); // silent bulk upload → wall bound only
341
467
  if (!r.ok)
342
468
  return { ok: false, error: new RemoteExecutionError("connect_failed", `workspace snapshot upload failed: ${this.redactUrl(r.error.message, url)}`, r.error) };
343
469
  if (r.value.exitCode !== 0)
@@ -345,12 +471,24 @@ export class RemoteK8sExecutionEnv {
345
471
  const name = this.podName;
346
472
  this.podName = undefined;
347
473
  this.podIP = undefined;
348
- await this.deletePod(name).catch(() => { });
474
+ await this.deletePod(name).catch(() => { }); // suspend = release the pod; the snapshot is the durable state
349
475
  return ok(snapshotId);
350
476
  }
477
+ /**
478
+ * Resume a workspace snapshot onto a FRESH pod (resume = executionEnvFactory rebuild + resumeVM):
479
+ * provision a pod, then download + untar the snapshot into the workspace. A missing object (empty/plan-only
480
+ * snapshot) resumes as a clean workspace.
481
+ *
482
+ * [1467]①(priorHandle 根保真)——k8s 走**诚实臂**而非绑定臂:pod 的 workspace volume 挂在**当前**
483
+ * 配置根(cfg.mountPath),往 priorHandle.mountPath 绑=往容器 rootfs untar 大工作区,ephemeral-storage
484
+ * 触顶即 pod 驱逐(恢复中途死,比「根变了 fail-closed reopen」更糟)。所以 untar 落当前根、handle 回报
485
+ * 真生效根(core 契约明许:「做不到就诚实返回实际 mountPath,勿回显未兑现值」);跨 K8S_MOUNT_PATH
486
+ * 变更窗的分歧由 core 侧 fail-closed reopen 兜底,审批不烧错根。
487
+ */
351
488
  async resumeVM(snapshotId, options) {
352
489
  if (!this.cfg.s3Snapshot)
353
490
  return unsupported("resumeVM");
491
+ // Validate + presign BEFORE provisioning a pod — a traversing/garbage snapshotId should fail fast, not leak a pod.
354
492
  const presigned = this.presignSnapshot("GET", snapshotId);
355
493
  if (!presigned.ok)
356
494
  return presigned;
@@ -358,11 +496,14 @@ export class RemoteK8sExecutionEnv {
358
496
  if (!c.ok)
359
497
  return c;
360
498
  const url = presigned.value;
499
+ // Capture the HTTP status (curl -s -w) and branch on it: 404 → empty/plan-only snapshot, leave the fresh
500
+ // empty workspace; non-200 → real error (exit 22); 200 → untar. (No `-f`: we need the status code, not a
501
+ // curl error exit, to distinguish the tolerated 404 from a genuine failure.)
361
502
  const cmd = `code=$(curl -s -o /tmp/ws.tgz -w '%{http_code}' ${shellQuote(url)}); ` +
362
503
  `if [ "$code" = "404" ]; then echo EMPTY_SNAPSHOT; exit 0; fi; ` +
363
504
  `if [ "$code" != "200" ]; then echo "snapshot GET HTTP $code" >&2; exit 22; fi; ` +
364
505
  `tar xzf /tmp/ws.tgz -C ${shellQuote(this.cfg.mountPath)} && rm -f /tmp/ws.tgz`;
365
- const r = await this.runExec(cmd, { timeoutMs: this.cfg.dataTimeoutMs, livenessMs: 0 });
506
+ const r = await this.runExec(cmd, { timeoutMs: this.cfg.dataTimeoutMs, livenessMs: 0 }); // silent bulk download → wall bound only
366
507
  if (!r.ok)
367
508
  return { ok: false, error: new RemoteExecutionError("connect_failed", `workspace snapshot restore failed: ${this.redactUrl(r.error.message, url)}`, r.error) };
368
509
  if (r.value.exitCode !== 0)
@@ -370,12 +511,19 @@ export class RemoteK8sExecutionEnv {
370
511
  this.handleSnapshotId = snapshotId;
371
512
  return ok(this.workspaceHandle());
372
513
  }
514
+ /**
515
+ * Honest post-resume consistency (design/48 §5 council #8): the workspace FILES are restored byte-for-byte
516
+ * from the snapshot, so read-before-edit hashing already lines up — there's nothing to re-fetch. In-VM
517
+ * process state (a language server) is gone by design; core/the LSP manager re-establishes it lazily. So
518
+ * this is a successful no-op rather than the previous `unsupported` (which forced a cold rebuild).
519
+ */
373
520
  async postResumeInit() {
374
521
  if (!this.cfg.s3Snapshot)
375
522
  return unsupported("postResumeInit");
376
523
  return ok(undefined);
377
524
  }
378
525
  async reconnect(_sessionToken) {
526
+ // The pod either still runs (reuse) or is gone (fresh connect).
379
527
  if (this.podName) {
380
528
  const alive = await this.request("GET", `/api/v1/namespaces/${this.cfg.namespace}/pods/${this.podName}`).catch(() => undefined);
381
529
  const phase = alive?.json?.status?.phase;
@@ -389,7 +537,10 @@ export class RemoteK8sExecutionEnv {
389
537
  async deletePod(name) {
390
538
  await this.request("DELETE", `/api/v1/namespaces/${this.cfg.namespace}/pods/${name}?gracePeriodSeconds=0`, undefined, { timeoutMs: this.cfg.rpcTimeoutMs });
391
539
  }
540
+ /** Delete the sandbox pod — the workspace is DISPOSABLE (vs SSH/ADB disconnect-only). Idempotent, never throws. */
392
541
  async destroy() {
542
+ // design/103 §3.7 backstop: kill/clear any background jobs while the pod is still alive (core's run-loop finally
543
+ // usually disposed already → this iterates an empty registry). After this the pod delete reaps anything left.
393
544
  await this.disposeBackgroundShells().catch(() => { });
394
545
  this.destroyed = true;
395
546
  const name = this.podName;
@@ -401,6 +552,7 @@ export class RemoteK8sExecutionEnv {
401
552
  async cleanup() {
402
553
  return this.destroy();
403
554
  }
555
+ // ─────────────────────────────── exec ───────────────────────────────
404
556
  execUrl(command) {
405
557
  const { apiUrl, token } = this.creds();
406
558
  const base = apiUrl.replace(/^http/, "ws");
@@ -412,7 +564,18 @@ export class RemoteK8sExecutionEnv {
412
564
  q.append("command", part);
413
565
  return { url: `${base}/api/v1/namespaces/${this.cfg.namespace}/pods/${this.podName}/exec?${q.toString()}`, headers: { authorization: `Bearer ${token}` } };
414
566
  }
415
- runExec(command, opts) {
567
+ /**
568
+ * Run a command over the exec WebSocket. Frames: byte0=channel (1 stdout / 2 stderr / 3 status JSON with the
569
+ * REAL exit code). Bounds: caller wall-clock (explicit `timeout`) + zero-progress liveness (`livenessMs`) —
570
+ * a socket killed by either does NOT kill the remote process (disposable-pod posture, see header).
571
+ */
572
+ runExec(command,
573
+ // 🔴 binary caveat (D2): runExec is SHARED by the model-facing TEXT exec / execStream (boundOutput:true —
574
+ // a `yes`/`cat big` must not OOM the durable worker) AND by readBinaryFile, which reads the file as base64 over
575
+ // execData→runExec and decodes r.value.stdout. Bounding that base64 stream would SILENTLY CORRUPT binary/large
576
+ // reads. So boundOutput DEFAULTS OFF: every data path (execData, snapshot tar up/down, mkdir) keeps EVERY byte
577
+ // byte-exact; ONLY exec()/execStream() opt in. stderr is always bounded (never the file payload).
578
+ opts) {
416
579
  let sock;
417
580
  let headers;
418
581
  let url;
@@ -424,19 +587,27 @@ export class RemoteK8sExecutionEnv {
424
587
  return Promise.resolve({ ok: false, error: new ExecutionError("shell_unavailable", e instanceof Error ? e.message : String(e), e instanceof Error ? e : undefined) });
425
588
  }
426
589
  return new Promise((resolvePromise) => {
590
+ // boundOutput (see signature): TEXT exec/execStream bound stdout to a rolling-TAIL (last 8MB + dropped marker,
591
+ // O(maxBytes)); data paths (boundOutput off) keep full bytes so readBinaryFile's base64 decodes losslessly.
592
+ // stderr is ALWAYS bounded — it never carries the base64 file payload.
427
593
  const boundOutput = !!opts?.boundOutput;
428
- const outBufs = [];
429
- const outTail = boundOutput ? new RollingTailBuffer() : undefined;
594
+ const outBufs = []; // data path (boundOutput off): full bytes
595
+ const outTail = boundOutput ? new RollingTailBuffer() : undefined; // text path: bounded stdout
430
596
  const errBuf = new RollingTailBuffer();
597
+ // Per-channel UTF-8 decoders for the STREAMING callbacks only (council BUG#1): a multibyte codepoint can
598
+ // straddle two WS frames → a per-frame `payload.toString("utf8")` emits U+FFFD. StringDecoder buffers the
599
+ // incomplete tail across writes. (The buffered path decodes once via Buffer.concat below — already safe.)
431
600
  const outDecoder = opts?.onStdout ? new StringDecoder("utf8") : undefined;
432
601
  const errDecoder = opts?.onStderr ? new StringDecoder("utf8") : undefined;
433
602
  let statusFrame;
434
603
  let settled = false;
435
604
  let lastProgress = Date.now();
605
+ // design/116 detach: once set, the socket's frames belong to the ADOPTED shell — message/error/close route
606
+ // here instead of the (already-settled) foreground buffers, and the socket is NOT closed on settle.
436
607
  let spliceSink;
437
- let sinkClosed = false;
608
+ let sinkClosed = false; // error+close can BOTH fire — notify the adopted shell's terminal exactly once
438
609
  let removeDetachListener;
439
- const detachTee = opts?.detach?.tee;
610
+ const detachTee = opts?.detach?.tee; // fed on every fg frame; a later adoption takes the pair over as-is (review #3)
440
611
  const finish = (r) => {
441
612
  if (settled)
442
613
  return;
@@ -449,12 +620,19 @@ export class RemoteK8sExecutionEnv {
449
620
  (sock.terminate ?? sock.close).call(sock);
450
621
  }
451
622
  catch {
623
+ /* already gone */
452
624
  }
453
625
  resolvePromise(r);
454
626
  };
455
627
  const wallMs = opts?.timeoutMs;
456
628
  const wallTimer = wallMs ? setTimeout(() => finish({ ok: false, error: new ExecutionError("timeout", `command exceeded ${wallMs}ms`) }), wallMs) : undefined;
629
+ // zero-progress liveness (E2B class-fix semantics): on firing, re-check actual idleness and re-arm for
630
+ // EXACTLY the remaining window (the E2B waitWithLiveness/council-DESIGN-1 discipline — a full-window
631
+ // re-arm would let a hang sit for up to 2× livenessMs before detection).
457
632
  let livenessTimer;
633
+ // Per-call liveness override (0 = disabled): bulk data transfers (tar→S3 upload, S3→untar restore) are
634
+ // legitimately SILENT for their whole duration, so the zero-output liveness would false-positive a "hang";
635
+ // they rely on the wall-clock `timeoutMs` (dataTimeoutMs) instead. Defaults to the configured command bound.
458
636
  const livenessMs = opts?.livenessMs ?? this.cfg.livenessMs;
459
637
  const armLiveness = () => {
460
638
  if (livenessMs <= 0)
@@ -481,8 +659,10 @@ export class RemoteK8sExecutionEnv {
481
659
  return;
482
660
  lastProgress = Date.now();
483
661
  const channel = buf[0];
484
- const payload = buf.subarray(1);
662
+ const payload = buf.subarray(1); // zero-copy view; `ws` gives each message its own buffer (no reuse),
485
663
  if (spliceSink) {
664
+ // Adopted: output frames feed the background shell's drain buffers; the status frame still lands in
665
+ // `statusFrame` so the close handler can classify the adopted shell's terminal.
486
666
  if (channel === 1 || channel === 2)
487
667
  spliceSink.data(channel, payload);
488
668
  else if (channel === 3)
@@ -491,11 +671,11 @@ export class RemoteK8sExecutionEnv {
491
671
  }
492
672
  if (channel === 1) {
493
673
  if (boundOutput)
494
- outTail.push(payload);
674
+ outTail.push(payload); // text path: bounded tail (streaming callback below still gets the full chunk)
495
675
  else
496
- outBufs.push(payload);
676
+ outBufs.push(payload); // data path: full bytes, safe to retain until the final Buffer.concat (council perf#4)
497
677
  if (detachTee)
498
- feedMemStream(detachTee.out, payload, K8S_BG_MEM_CAP);
678
+ feedMemStream(detachTee.out, payload, K8S_BG_MEM_CAP); // tee-from-start (review #3): one decoder spans the detach instant
499
679
  if (outDecoder)
500
680
  opts.onStdout(outDecoder.write(payload));
501
681
  }
@@ -513,6 +693,8 @@ export class RemoteK8sExecutionEnv {
513
693
  sock.on("error", (...args) => {
514
694
  const e = args[0];
515
695
  if (spliceSink) {
696
+ // Adopted: a socket error mid-background = the output feed is lost; outcome unknown → failed. The
697
+ // process itself may still run in the pod (disposable-pod posture — teardown reaps it).
516
698
  if (!sinkClosed) {
517
699
  sinkClosed = true;
518
700
  spliceSink.closed({ failed: true });
@@ -526,12 +708,16 @@ export class RemoteK8sExecutionEnv {
526
708
  if (sinkClosed)
527
709
  return;
528
710
  sinkClosed = true;
711
+ // Terminal classification mirrors the launch-mode poll: a status frame with an exit code ⇒ exited
712
+ // (non-zero exit is a NORMAL result); no status ⇒ the transport died mid-background ⇒ failed.
529
713
  if (statusFrame === undefined)
530
714
  return spliceSink.closed({ failed: true });
531
715
  const parsed = exitCodeFromStatus(statusFrame);
532
716
  return spliceSink.closed("error" in parsed ? { failed: true } : { failed: false, exitCode: parsed.exitCode });
533
717
  }
534
718
  if (statusFrame === undefined) {
719
+ // closed without a status frame = the transport died mid-command, NOT a command result ([R78]#1).
720
+ // Typed "transport_lost" since core 1.89 (outcome UNKNOWN — retry only per the op's idempotency).
535
721
  finish({ ok: false, error: new ExecutionError("transport_lost", "exec socket closed without status — k8s transport lost mid-command") });
536
722
  return;
537
723
  }
@@ -547,21 +733,32 @@ export class RemoteK8sExecutionEnv {
547
733
  finish(ok({ stdout: markTruncated(out.text, out.droppedBytes), stderr, exitCode: parsed.exitCode }));
548
734
  }
549
735
  else {
736
+ // byte-exact: readBinaryFile decodes this stdout as base64; never truncate.
550
737
  finish(ok({ stdout: Buffer.concat(outBufs).toString("utf8"), stderr, exitCode: parsed.exitCode }));
551
738
  }
552
739
  });
740
+ // design/116 detach — wired AFTER every handler above exists (the host adapter's create-then-abort TDZ
741
+ // lesson): core's ToolDetachHub can hand an ALREADY-aborted signal (detach requested before the exec
742
+ // started), in which case onDetach runs synchronously right here.
553
743
  const detach = opts?.detach;
554
744
  if (detach) {
555
745
  const onDetach = () => {
556
746
  if (settled || spliceSink)
557
747
  return;
558
- const out = outTail ? outTail.result() : { text: "", droppedBytes: 0 };
748
+ const out = outTail ? outTail.result() : { text: "", droppedBytes: 0 }; // detach rides the exec path (boundOutput:true)
559
749
  const err = errBuf.result();
560
750
  const shellId = detach.adopt({ sock, reroute: (sink) => { spliceSink = sink; } });
561
751
  if (shellId === undefined) {
752
+ // Refused (limit / builder failure) → the foreground continues untouched. 🔴 review #5: `reroute` is a
753
+ // builder SIDE EFFECT — if the builder ran it and then threw, adoptSync's catch returns undefined but
754
+ // spliceSink is already set, which would silently reroute every subsequent fg frame into an unregistered
755
+ // buffer AND make this exec never settle (close would notify a sink nobody owns). Un-splice defensively;
756
+ // same-tick, so no frame can have landed in between.
562
757
  spliceSink = undefined;
563
758
  return;
564
759
  }
760
+ // Settle EARLY *without* closing the socket — the adopted shell owns it now. finish() is deliberately
761
+ // NOT used (it terminates the socket); this is its detach twin.
565
762
  settled = true;
566
763
  clearTimeout(wallTimer);
567
764
  clearTimeout(livenessTimer);
@@ -582,21 +779,29 @@ export class RemoteK8sExecutionEnv {
582
779
  const c = await this.ensureConnected();
583
780
  if (!c.ok)
584
781
  return c;
782
+ // design/116 detach (core 1.207, 1.82): the Bash tool threads a `detachSignal` for every exec once
783
+ // `supportsDetach` is advertised. `ExecOpts` doesn't type it yet (core-side seam) — read it structurally.
585
784
  const detachSignal = options?.detachSignal;
586
- this.inFlightExecs++;
785
+ this.inFlightExecs++; // suspendVM refuses while > 0 (council #4: never snapshot a running command)
587
786
  try {
588
787
  const composed = this.withCwdEnv(command, options?.cwd, options?.env);
589
788
  if (!detachSignal) {
590
789
  return await this.runExec(composed, {
591
790
  signal: options?.abortSignal,
592
791
  timeoutMs: options?.timeout != null ? options.timeout * 1000 : undefined,
593
- boundOutput: true,
792
+ boundOutput: true, // model-facing TEXT exec — bound the buffered return value (D2)
594
793
  onStdout: options?.onStdout,
595
794
  onStderr: options?.onStderr,
596
795
  });
597
796
  }
797
+ // Detach-capable exec: the pgid prelude (buildDetachCapableExec) makes the running command adoptable —
798
+ // kill/dispose on the adopted shell reuse the SAME pod-side scripts as a spawned one. On detach, adoptSync
799
+ // splices the still-open exec socket into memory-mode drain buffers (SYNCHRONOUS — no await between the
800
+ // tee handover and the reroute, the host adapter's double-review contract).
598
801
  const adoptId = randomUUID();
599
802
  const dir = `${K8S_BG_DIR_ROOT}/exec-${adoptId}`;
803
+ // tee-from-start (Fable-5 review #3): runExec feeds every fg frame into this pair through its per-stream
804
+ // StringDecoder; adoption takes the pair over as-is, so ONE decoder spans the detach instant (no U+FFFD seam).
600
805
  const tee = { out: seedMemStream(""), err: seedMemStream("") };
601
806
  return await this.runExec(buildDetachCapableExec(dir, composed), {
602
807
  signal: options?.abortSignal,
@@ -608,8 +813,11 @@ export class RemoteK8sExecutionEnv {
608
813
  signal: detachSignal,
609
814
  tee,
610
815
  adopt: (io) => {
816
+ // 🔴 builder contract (review #5): everything inside is NON-THROWING (object construction + the reroute
817
+ // assignment). adoptSync's catch does NOT undo side effects — future logic that can throw must go before
818
+ // `io.reroute` (the runExec caller also un-splices defensively on an undefined return).
611
819
  const adopted = this.bgManager.adoptSync((ctx) => {
612
- const mem = { out: tee.out, err: tee.err };
820
+ const mem = { out: tee.out, err: tee.err }; // the tee already holds the full fg history (truncation marker rides on the exec's OWN early result only — review LOW)
613
821
  io.reroute({
614
822
  data: (channel, payload) => feedMemStream(channel === 1 ? mem.out : mem.err, payload, K8S_BG_MEM_CAP),
615
823
  closed: (terminal) => ctx.onTerminal(terminal.failed, terminal.exitCode),
@@ -625,13 +833,28 @@ export class RemoteK8sExecutionEnv {
625
833
  this.inFlightExecs--;
626
834
  }
627
835
  }
836
+ /**
837
+ * LSP surface (lsp/manager.ts `isLspCapable` duck-type, design/64 §13.1): cluster-internal address for a
838
+ * port on the sandbox pod. The E2B counterpart returns a public proxy host; a pod has none — the WORKER
839
+ * must be able to reach pod IPs (in-cluster deployment, or on-node), and the transport is plain `ws://`
840
+ * (cluster network, no TLS — the per-session bridge token still gates access like on E2B).
841
+ */
628
842
  async getHost(port) {
629
843
  const c = await this.ensureConnected();
630
844
  if (!c.ok || !this.podIP)
631
- return undefined;
845
+ return undefined; // no IP captured (e.g. faked/older apiserver) → LSP degrades
632
846
  return `${this.podIP}:${port}`;
633
847
  }
848
+ /**
849
+ * Start a long-lived helper (the LSP bridge) detached from this exec session. Unlike E2B — which reaps a
850
+ * foreground command's process group on completion, forcing a native background API — k8s exec just closes
851
+ * the session: a `nohup`'d child with detached stdio survives, reparented to the container's PID 1 (the
852
+ * gate#2 background-gradle runs proved this through the production path). Output goes to /tmp for postmortem.
853
+ */
634
854
  async startBackground(command) {
855
+ // `command` is a COMPOUND shell line (lspStartCommand: `cd … && ENV=… node bridge.cjs`) — nohup takes a
856
+ // single executable, so wrap the whole line in `sh -c`. Caught live: `nohup cd …` silently started nothing
857
+ // and the bridge connect timed out (gate#2 k8s-LSP attempt 1).
635
858
  const wrapped = command.replace(/[\\"$`]/g, (c) => "\\" + c);
636
859
  const r = await this.exec(`(nohup sh -c "${wrapped}" >>/tmp/lsp-bridge.log 2>&1 &) ; echo started`, { timeout: 30 });
637
860
  if (!r.ok)
@@ -643,7 +866,7 @@ export class RemoteK8sExecutionEnv {
643
866
  const c = await this.ensureConnected();
644
867
  if (!c.ok)
645
868
  throw new RemoteExecutionError("connect_failed", c.error.message, c.error);
646
- this.inFlightExecs++;
869
+ this.inFlightExecs++; // suspendVM refuses while > 0 (council #4); decremented in the finally below
647
870
  const queue = [];
648
871
  let finished = false;
649
872
  let failure;
@@ -656,12 +879,21 @@ export class RemoteK8sExecutionEnv {
656
879
  };
657
880
  let lastChunk = Date.now();
658
881
  const idleMs = options?.readTimeoutMs ?? this.cfg.livenessMs;
882
+ // Total-output cap (D2): parity with the host/local-docker execStream — when the caller sets
883
+ // maxOutputBytes, count yielded bytes and abort+throw past the cap (the finally's ac.abort() then closes
884
+ // the exec socket). Previously k8s execStream IGNORED maxOutputBytes (it was the only adapter that did).
659
885
  const cap = options?.maxOutputBytes;
660
886
  let bytes = 0;
887
+ // Internal abort composed with the caller's: EVERY generator exit (normal end, idle-throw, consumer
888
+ // early-break via .return()) must settle runExec IMMEDIATELY — without this, the finally's `await done`
889
+ // would sit until runExec's own liveness fired (~minutes) with the exec socket held open (self-review A).
661
890
  const ac = new AbortController();
662
891
  const signal = options?.signal ? AbortSignal.any([options.signal, ac.signal]) : ac.signal;
663
892
  const done = this.runExec(this.withCwdEnv(command, options?.cwd, options?.env), {
664
893
  signal,
894
+ // The consumer reads from the onStdout/onStderr callbacks (full chunks); runExec's retained buffer is
895
+ // discarded here (only exitCode is used). boundOutput:true caps that otherwise-unbounded retained buffer
896
+ // with zero change to what the stream consumer sees (D2).
665
897
  boundOutput: true,
666
898
  onStdout: (d) => {
667
899
  lastChunk = Date.now();
@@ -677,9 +909,12 @@ export class RemoteK8sExecutionEnv {
677
909
  if (r.ok)
678
910
  exitCode = r.value.exitCode;
679
911
  else {
912
+ // Map the buffered-exec ExecutionErrorCode → a RemoteExecutionErrorCode. `spawn_error`/`shell_unavailable`
913
+ // mean the environment is fundamentally broken → "unknown" (non-retryable), NOT "connect_failed" (which
914
+ // the retry whitelist would re-attempt against a broken shell — council BUG#2).
680
915
  const code = r.error.code === "aborted"
681
916
  ? "aborted"
682
- : r.error.code === "transport_lost"
917
+ : r.error.code === "transport_lost" // typed end-to-end since core 1.89 (was a message marker)
683
918
  ? "transport_lost"
684
919
  : r.error.code === "timeout"
685
920
  ? "timeout"
@@ -720,11 +955,23 @@ export class RemoteK8sExecutionEnv {
720
955
  yield { type: "exit", exitCode };
721
956
  }
722
957
  finally {
723
- ac.abort();
958
+ ac.abort(); // settle runExec now (no-op if already finished) — closes the socket, frees the timers
724
959
  await done.catch(() => { });
725
960
  this.inFlightExecs--;
726
961
  }
727
962
  }
963
+ // ─────────────────────────── design/103 background shell (TOB, path ①) ───────────────────────────
964
+ //
965
+ // kata/k8s takes path ① (design/103 §5.2): the pod filesystem buffers output, re-read by byte cursor each poll
966
+ // — unlike E2B there is no provider job handle, and closing the exec socket does NOT kill the remote process
967
+ // (disposable-pod posture, see file header), so termination is detected by an exit-sentinel file and kill goes
968
+ // through the process GROUP (`setsid` + self-reported pgid). Background ops run as short ephemeral execs and
969
+ // deliberately do NOT touch `inFlightExecs` (they are control-plane management, not a foreground command to
970
+ // snapshot; the detached job is killed before `suspendVM` by the core dispose saga, design/103 §3.7).
971
+ /**
972
+ * A background job cannot outlive its pod (`activeDeadlineSeconds` ≈ `timeoutMs`), so the BG ceiling is clamped to
973
+ * it (assigned in the constructor — `this.cfg` is set there, so a field initializer would read it pre-init).
974
+ */
728
975
  backgroundCapabilities;
729
976
  _bgManager;
730
977
  get bgManager() {
@@ -737,9 +984,11 @@ export class RemoteK8sExecutionEnv {
737
984
  async launch(command, options, ctx) {
738
985
  const conn = await self.ensureConnected();
739
986
  if (!conn.ok)
740
- throw new Error(`pod connect failed: ${conn.error.message}`);
987
+ throw new Error(`pod connect failed: ${conn.error.message}`); // → manager wraps as spawn_failed
741
988
  const jobId = randomUUID();
742
989
  const dir = `${K8S_BG_DIR_ROOT}/${jobId}`;
990
+ // 🔴 secret-scrub red line (design/103 §3.1): cwd + env injected via the SAME `withCwdEnv` path as exec —
991
+ // the host process.env is never injected; only the scrubbed task env core passes reaches the child.
743
992
  const inner = self.withCwdEnv(command, options?.cwd, options?.env);
744
993
  const cmdB64 = Buffer.from(inner, "utf8").toString("base64");
745
994
  const runnerB64 = Buffer.from(buildBgRunnerScript(dir), "utf8").toString("base64");
@@ -749,10 +998,12 @@ export class RemoteK8sExecutionEnv {
749
998
  if (r.value.exitCode !== 0 || !r.value.stdout.includes("SEMABG_LAUNCHED")) {
750
999
  throw new Error(`background launch exited ${r.value.exitCode}: ${r.value.stderr.slice(0, 200)}`);
751
1000
  }
752
- void ctx;
1001
+ void ctx; // k8s is pull-model: termination is discovered in read(); ctx.onTerminal is intentionally unused
753
1002
  return { jobId, dir, stdoutCursor: 0, stderrCursor: 0 };
754
1003
  },
755
1004
  async read(state) {
1005
+ // ADOPT (memory) mode: the still-open exec socket feeds the drain buffers; termination is PUSH-model
1006
+ // (socket close → ctx.onTerminal), so `read` never reports terminal here — mirror of the E2B/host adopt.
756
1007
  if (state.adopt) {
757
1008
  const o = drainMemStream(state.adopt.mem.out);
758
1009
  const e = drainMemStream(state.adopt.mem.err);
@@ -770,15 +1021,20 @@ export class RemoteK8sExecutionEnv {
770
1021
  if (!p)
771
1022
  throw new Error("background poll: unparseable response");
772
1023
  if (p.gone) {
1024
+ // dir vanished while still owned (dispose clears the registry FIRST, so this isn't us) → abnormal end.
773
1025
  return { stdout: "", stderr: "", bytesFromStart: state.stdoutCursor + state.stderrCursor, bytesDroppedBeforeCursor: 0, terminal: { failed: true } };
774
1026
  }
775
1027
  const droppedO = Math.max(0, p.outTotal - state.stdoutCursor - p.stdoutBytes);
776
1028
  const droppedE = Math.max(0, p.errTotal - state.stderrCursor - p.stderrBytes);
777
1029
  state.stdoutCursor = p.outTotal;
778
1030
  state.stderrCursor = p.errTotal;
1031
+ // Terminal: exit sentinel present → exited (exit code known); else process group gone without a sentinel →
1032
+ // failed (abnormal — a clean exit always writes the sentinel as the runner's last act before the group dies).
779
1033
  let terminal = p.exitCode !== undefined ? { failed: false, exitCode: p.exitCode } : !p.alive ? { failed: true } : undefined;
1034
+ // 🔴 disk guard: direct redirect has no in-pod cap, so reap a runaway whose output exceeds K8S_BG_FILE_CAP
1035
+ // (the kata VM isolates the blast to this disposable pod; the BG-timeout + activeDeadlineSeconds also bound it).
780
1036
  if (!terminal && (p.outTotal > K8S_BG_FILE_CAP || p.errTotal > K8S_BG_FILE_CAP)) {
781
- void bgExec(buildBgKillScript(state.dir), true).catch(() => { });
1037
+ void bgExec(buildBgKillScript(state.dir), true).catch(() => { }); // fire-and-forget reap
782
1038
  terminal = { failed: true };
783
1039
  }
784
1040
  return {
@@ -790,21 +1046,23 @@ export class RemoteK8sExecutionEnv {
790
1046
  };
791
1047
  },
792
1048
  async kill(state) {
793
- await bgExec(buildBgKillScript(state.dir), true).catch(() => { });
1049
+ await bgExec(buildBgKillScript(state.dir), true).catch(() => { }); // best-effort, idempotent
794
1050
  },
795
1051
  async disposeOne(state) {
1052
+ // Adopted shells hold the (kept-open) exec socket — release it on EVERY dispose path, even when the pod
1053
+ // is already being torn down (the socket is a worker-side resource, not a pod-side one).
796
1054
  const closeAdopted = () => {
797
1055
  if (!state.adopt)
798
1056
  return;
799
1057
  try {
800
1058
  (state.adopt.sock.terminate ?? state.adopt.sock.close).call(state.adopt.sock);
801
1059
  }
802
- catch { }
1060
+ catch { /* already gone */ }
803
1061
  };
804
1062
  if (self.destroyed) {
805
1063
  closeAdopted();
806
1064
  return;
807
- }
1065
+ } // pod teardown reaps the job; don't resurrect a connection
808
1066
  await bgExec(buildBgDisposeScript(state.dir), true).catch(() => { });
809
1067
  closeAdopted();
810
1068
  },
@@ -820,8 +1078,11 @@ export class RemoteK8sExecutionEnv {
820
1078
  return this.bgManager.kill(shellId);
821
1079
  }
822
1080
  disposeBackgroundShells(opts) {
1081
+ // 签名统一带 opts(core 尾调 {except: keepAlive})。隔离 env 的留活是名义的——沙箱拆机进程必然随之
1082
+ // 消亡(destroy 整体带走),core 的 envDying settle 会先给准确 killed 回执;透传保证签名不静默丢参。
823
1083
  return this._bgManager ? this._bgManager.dispose(opts) : Promise.resolve();
824
1084
  }
1085
+ // ─────────────────────────────── FileSystem (over exec; real exit codes) ───────────────────────────────
825
1086
  async absolutePath(p) {
826
1087
  return ok(this.resolve(p));
827
1088
  }
@@ -830,9 +1091,11 @@ export class RemoteK8sExecutionEnv {
830
1091
  }
831
1092
  async readBinaryFile(p, abortSignal) {
832
1093
  const abs = this.resolve(p);
1094
+ // base64 keeps the WS text-safe; the REAL exit code separates "empty file" from "read failed".
833
1095
  const r = await this.execData(`base64 < ${shellQuote(abs)}`, abortSignal);
834
1096
  if (!r.ok)
835
1097
  return { ok: false, error: fileErrorFromExec(r.error, abs) };
1098
+ // Non-zero base64 read → central classifier, not_found fallback (truncated-stderr tolerant — council BUG#3).
836
1099
  if (r.value.exitCode !== 0)
837
1100
  return { ok: false, error: classifyFsStderr(r.value.stderr, abs, "not_found") };
838
1101
  return ok(new Uint8Array(Buffer.from(r.value.stdout.replace(/\s+/g, ""), "base64")));
@@ -852,6 +1115,11 @@ export class RemoteK8sExecutionEnv {
852
1115
  lines = lines.slice(0, options.maxLines);
853
1116
  return ok(lines);
854
1117
  }
1118
+ /** 父目录自建(与 host/local-docker/adb/ssh 同口径)。抽成共用是因为 `appendFile` 曾**绕过**它:
1119
+ * 🔴 2026-07-25 实测的跨腿分叉 —— `appendFile` 直接调 `writeChunked`,整条路上没有 `mkdir -p`,于是
1120
+ * 「直接 append 建一个新日志路径」在 k8s 腿上返 `not_found`,而其余 5 腿都成功(host/local-docker/adb 实测,
1121
+ * ssh/e2b 由方法体判据)。core 的后台输出镜像正是"每拍 append 一次"的形,这条分叉会让它在 k8s 腿上从第一拍
1122
+ * 就失败,而 `mirrorFailed` 只会表现成一句「output file is INCOMPLETE」。 */
855
1123
  async ensureParentDir(abs, abortSignal) {
856
1124
  const dir = path.posix.dirname(abs);
857
1125
  if (!dir || dir === "." || dir === "/")
@@ -868,15 +1136,21 @@ export class RemoteK8sExecutionEnv {
868
1136
  const bad = await this.ensureParentDir(abs, abortSignal);
869
1137
  if (bad)
870
1138
  return bad;
871
- return this.writeChunked(abs, content, true, abortSignal);
1139
+ return this.writeChunked(abs, content, true, abortSignal); // truncate first
872
1140
  }
873
1141
  async appendFile(p, content, abortSignal) {
874
1142
  const abs = this.resolve(p);
875
- const bad = await this.ensureParentDir(abs, abortSignal);
1143
+ const bad = await this.ensureParentDir(abs, abortSignal); // 见 ensureParentDir 旁注:此前这条路漏了它
876
1144
  if (bad)
877
1145
  return bad;
878
- return this.writeChunked(abs, content, false, abortSignal);
1146
+ return this.writeChunked(abs, content, false, abortSignal); // append from byte 0
879
1147
  }
1148
+ /**
1149
+ * Write `content` to `abs` via argv-embedded base64, chunked at WRITE_CHUNK_BYTES (no stdin on the exec
1150
+ * channel we use; v4 has no per-channel close). `truncateFirst` → the FIRST chunk uses `>` (overwrite),
1151
+ * the rest `>>` (append); `false` → all chunks `>>`. Empty content still runs exactly one command so an
1152
+ * empty file is created (write) / left intact (append). Shared by writeFile/appendFile (council DESIGN#2).
1153
+ */
880
1154
  async writeChunked(abs, content, truncateFirst, abortSignal) {
881
1155
  const bytes = typeof content === "string" ? Buffer.from(content, "utf8") : Buffer.from(content);
882
1156
  for (let off = 0, first = true; off < bytes.length || first; off += WRITE_CHUNK_BYTES, first = false) {
@@ -890,6 +1164,9 @@ export class RemoteK8sExecutionEnv {
890
1164
  }
891
1165
  return ok(undefined);
892
1166
  }
1167
+ // ── design/158 B6:POSIX shell-out 文件面全部委托共享基座(posix-shell-fs.ts)─────────────────────
1168
+ // 六裁决(`--`/classifyFsStderr/ls -1A/mv 后缀形/temp 位置参数化/`[ -e ]||[ -L ]` exists)与
1169
+ // readLink・lstat 契约见基座头注;本腿此前的 fileErrorFrom 分类形与 exists 形即基座正形的出处。
893
1170
  posixFsInst;
894
1171
  get posixFs() {
895
1172
  return (this.posixFsInst ??= createPosixShellFs({
@@ -925,6 +1202,8 @@ export class RemoteK8sExecutionEnv {
925
1202
  async createTempFile(options) {
926
1203
  return this.posixFs.createTempFile(options);
927
1204
  }
1205
+ // ─────────────────────────────── helpers ───────────────────────────────
1206
+ /** exec with the data-transfer bound (large reads/writes legitimately run longer than a control op). */
928
1207
  async execData(command, abortSignal) {
929
1208
  const c = await this.ensureConnected();
930
1209
  if (!c.ok)
@@ -943,7 +1222,14 @@ export class RemoteK8sExecutionEnv {
943
1222
  return parts.join(" ");
944
1223
  }
945
1224
  }
1225
+ /**
1226
+ * `ExecutionEnvFactory` for the k8s pod-sandbox backend — one fresh (unconnected) env per task; the pod is
1227
+ * created lazily on connect and deleted by `destroy()` (Runner-owned lifetime; `activeDeadlineSeconds` is the
1228
+ * leak guard if destroy never runs).
1229
+ */
946
1230
  export function k8sExecutionEnvFactory(config) {
1231
+ // Thread the task's sessionId so workspace-snapshot keys are namespaced per session
1232
+ // (workspaces/<prefix>/<sessionId>/<snapshotId>.tar.gz).
947
1233
  return (ctx) => new RemoteK8sExecutionEnv({ ...config, sessionId: ctx.sessionId });
948
1234
  }
949
1235
  //# sourceMappingURL=remote-env-k8s.js.map