@sema-agent/server 1.323.0 → 2.0.1

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 (565) 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/budget-tracing.d.ts +48 -0
  46. package/dist/boot/budget-tracing.js +86 -0
  47. package/dist/boot/config-center.d.ts +62 -0
  48. package/dist/boot/config-center.js +995 -0
  49. package/dist/boot/coordinators.d.ts +33 -0
  50. package/dist/boot/coordinators.js +97 -0
  51. package/dist/boot/execution-env.d.ts +26 -0
  52. package/dist/boot/execution-env.js +370 -0
  53. package/dist/boot/leader.d.ts +27 -0
  54. package/dist/boot/leader.js +81 -0
  55. package/dist/boot/reapers.d.ts +53 -0
  56. package/dist/boot/reapers.js +252 -0
  57. package/dist/boot/resolve-spec.d.ts +70 -0
  58. package/dist/boot/resolve-spec.js +1072 -0
  59. package/dist/boot/runner-deps.d.ts +101 -0
  60. package/dist/boot/runner-deps.js +343 -0
  61. package/dist/boot/runtime-caps.d.ts +21 -0
  62. package/dist/boot/runtime-caps.js +62 -0
  63. package/dist/boot/session-faces.d.ts +57 -0
  64. package/dist/boot/session-faces.js +157 -0
  65. package/dist/boot/shutdown.d.ts +50 -0
  66. package/dist/boot/shutdown.js +129 -0
  67. package/dist/boot/stores.d.ts +32 -0
  68. package/dist/boot/stores.js +361 -0
  69. package/dist/boot/workflow-orchestration.d.ts +46 -0
  70. package/dist/boot/workflow-orchestration.js +150 -0
  71. package/dist/boot-reclaim.d.ts +19 -0
  72. package/dist/boot-reclaim.js +20 -0
  73. package/dist/brain.d.ts +25 -0
  74. package/dist/brain.js +61 -1
  75. package/dist/budget.d.ts +66 -0
  76. package/dist/budget.js +128 -4
  77. package/dist/capabilities/builtin-tools.d.ts +3 -0
  78. package/dist/capabilities/builtin-tools.js +3 -0
  79. package/dist/capabilities/center-plugins.d.ts +17 -1
  80. package/dist/capabilities/center-plugins.js +45 -4
  81. package/dist/capabilities/center-prompts.d.ts +46 -0
  82. package/dist/capabilities/center-prompts.js +57 -3
  83. package/dist/capabilities/code-review-council.d.ts +28 -0
  84. package/dist/capabilities/code-review-council.js +40 -7
  85. package/dist/capabilities/collab-wire.d.ts +2 -0
  86. package/dist/capabilities/collab-workflows.d.ts +7 -0
  87. package/dist/capabilities/collab-workflows.js +44 -1
  88. package/dist/capabilities/oa-tools.d.ts +6 -0
  89. package/dist/capabilities/oa-tools.js +10 -0
  90. package/dist/capabilities/prompt.d.ts +14 -0
  91. package/dist/capabilities/prompt.js +14 -0
  92. package/dist/capabilities/prompts/code-review.d.ts +14 -0
  93. package/dist/capabilities/prompts/code-review.js +14 -0
  94. package/dist/capabilities/prompts/identity.d.ts +6 -0
  95. package/dist/capabilities/prompts/identity.js +6 -0
  96. package/dist/capabilities/prompts/team.d.ts +4 -0
  97. package/dist/capabilities/prompts/team.js +4 -0
  98. package/dist/capabilities/repo-tools.d.ts +11 -0
  99. package/dist/capabilities/repo-tools.js +8 -0
  100. package/dist/capabilities/sandbox-file-send.d.ts +95 -0
  101. package/dist/capabilities/sandbox-file-send.js +91 -3
  102. package/dist/capabilities/scenarios.d.ts +130 -0
  103. package/dist/capabilities/scenarios.js +180 -7
  104. package/dist/capabilities/select-environment-tool.d.ts +7 -0
  105. package/dist/capabilities/select-environment-tool.js +30 -0
  106. package/dist/capabilities/send-user-file-tool.d.ts +33 -0
  107. package/dist/capabilities/send-user-file-tool.js +65 -1
  108. package/dist/capabilities/skills.d.ts +29 -0
  109. package/dist/capabilities/skills.js +13 -2
  110. package/dist/capabilities/team.d.ts +22 -0
  111. package/dist/capabilities/team.js +22 -2
  112. package/dist/capabilities/tool-defer.d.ts +26 -0
  113. package/dist/capabilities/tool-defer.js +3 -0
  114. package/dist/config-center/apply-effective.d.ts +65 -0
  115. package/dist/config-center/apply-effective.js +204 -12
  116. package/dist/config-center/http-client.d.ts +34 -0
  117. package/dist/config-center/http-client.js +68 -0
  118. package/dist/config-center/restart-signal.d.ts +22 -0
  119. package/dist/config-center/restart-signal.js +39 -0
  120. package/dist/config-center/skills-mcp.d.ts +20 -1
  121. package/dist/config-center/skills-mcp.js +42 -7
  122. package/dist/config-center/types.d.ts +92 -1
  123. package/dist/config-lkg.d.ts +12 -0
  124. package/dist/config-lkg.js +41 -0
  125. package/dist/config-provider.d.ts +78 -0
  126. package/dist/config-provider.js +165 -2
  127. package/dist/config-types.d.ts +585 -1
  128. package/dist/config.d.ts +78 -2
  129. package/dist/config.js +874 -301
  130. package/dist/degenerate-instrument.d.ts +38 -0
  131. package/dist/degenerate-instrument.js +55 -3
  132. package/dist/elicitation.d.ts +39 -0
  133. package/dist/elicitation.js +87 -3
  134. package/dist/env-facts.d.ts +61 -0
  135. package/dist/env-facts.js +73 -7
  136. package/dist/fleet/fleet-bus.d.ts +185 -0
  137. package/dist/fleet/fleet-bus.js +333 -27
  138. package/dist/fleet/subagent-tail-bus.d.ts +11 -0
  139. package/dist/fleet/subagent-tail-bus.js +33 -2
  140. package/dist/fleet-client.d.ts +38 -0
  141. package/dist/fleet-client.js +44 -9
  142. package/dist/fleet-lease.d.ts +46 -0
  143. package/dist/fleet-lease.js +73 -5
  144. package/dist/hooks/branch-transcript.d.ts +8 -0
  145. package/dist/hooks/branch-transcript.js +33 -0
  146. package/dist/hooks/cc-stop-prompt.d.ts +42 -0
  147. package/dist/hooks/cc-stop-prompt.js +43 -1
  148. package/dist/hooks/hook-llm.d.ts +15 -0
  149. package/dist/hooks/hook-llm.js +47 -0
  150. package/dist/hooks/hook-runner.d.ts +89 -0
  151. package/dist/hooks/hook-runner.js +327 -18
  152. package/dist/http/idempotency.d.ts +31 -0
  153. package/dist/http/idempotency.js +34 -0
  154. package/dist/http/principal-gate.d.ts +17 -0
  155. package/dist/http/principal-gate.js +35 -3
  156. package/dist/http/route-ctx.d.ts +36 -0
  157. package/dist/http/routes/approvals-assistant.d.ts +25 -0
  158. package/dist/http/routes/approvals-assistant.js +253 -23
  159. package/dist/http/routes/attachments.js +22 -4
  160. package/dist/http/routes/capabilities.d.ts +7 -0
  161. package/dist/http/routes/capabilities.js +202 -1
  162. package/dist/http/routes/fleet.d.ts +6 -0
  163. package/dist/http/routes/fleet.js +94 -5
  164. package/dist/http/routes/images.d.ts +17 -0
  165. package/dist/http/routes/images.js +222 -14
  166. package/dist/http/routes/leader.d.ts +7 -0
  167. package/dist/http/routes/leader.js +10 -0
  168. package/dist/http/routes/memory-policy.d.ts +6 -0
  169. package/dist/http/routes/memory-policy.js +38 -6
  170. package/dist/http/routes/notify-wake.d.ts +10 -0
  171. package/dist/http/routes/notify-wake.js +19 -3
  172. package/dist/http/routes/observability.d.ts +6 -0
  173. package/dist/http/routes/observability.js +19 -2
  174. package/dist/http/routes/runs.d.ts +15 -0
  175. package/dist/http/routes/runs.js +464 -41
  176. package/dist/http/routes/session-sync.d.ts +15 -0
  177. package/dist/http/routes/session-sync.js +229 -19
  178. package/dist/http/routes/sessions-list.d.ts +6 -0
  179. package/dist/http/routes/sessions-list.js +35 -4
  180. package/dist/http/routes/sessions.d.ts +14 -0
  181. package/dist/http/routes/sessions.js +176 -31
  182. package/dist/http/routes/side-query.d.ts +8 -0
  183. package/dist/http/routes/side-query.js +26 -1
  184. package/dist/http/routes/tasks.d.ts +18 -0
  185. package/dist/http/routes/tasks.js +501 -38
  186. package/dist/http/routes/trace-usage.d.ts +10 -0
  187. package/dist/http/routes/trace-usage.js +66 -14
  188. package/dist/http/routes/workflows.d.ts +12 -0
  189. package/dist/http/routes/workflows.js +180 -19
  190. package/dist/http/run-meta.d.ts +11 -0
  191. package/dist/http/run-meta.js +6 -0
  192. package/dist/http/send.d.ts +19 -0
  193. package/dist/http/send.js +11 -0
  194. package/dist/http/server.d.ts +255 -0
  195. package/dist/http/server.js +921 -75
  196. package/dist/http/sse-log.d.ts +15 -0
  197. package/dist/http/sse-log.js +16 -0
  198. package/dist/http/tar.d.ts +9 -0
  199. package/dist/http/tar.js +21 -5
  200. package/dist/http/wire-gate.d.ts +9 -0
  201. package/dist/http/wire-gate.js +9 -0
  202. package/dist/http/wire-types.d.ts +187 -0
  203. package/dist/http/workspace-content.d.ts +8 -0
  204. package/dist/http/workspace-content.js +10 -0
  205. package/dist/images/bake-validate.d.ts +63 -0
  206. package/dist/images/bake-validate.js +71 -1
  207. package/dist/images/manifest.d.ts +19 -0
  208. package/dist/images/manifest.js +6 -1
  209. package/dist/index.d.ts +1 -5
  210. package/dist/index.js +10 -5
  211. package/dist/key-resolver.d.ts +23 -0
  212. package/dist/key-resolver.js +27 -2
  213. package/dist/leader/diffout.d.ts +23 -0
  214. package/dist/leader/diffout.js +8 -1
  215. package/dist/leader/diffup.d.ts +32 -0
  216. package/dist/leader/diffup.js +43 -0
  217. package/dist/leader/endpoint.d.ts +20 -0
  218. package/dist/leader/endpoint.js +25 -2
  219. package/dist/leader/fanout.d.ts +60 -0
  220. package/dist/leader/fanout.js +29 -3
  221. package/dist/leader/grader-env-factory.d.ts +74 -0
  222. package/dist/leader/grader-env-factory.js +62 -3
  223. package/dist/leader/leader.d.ts +97 -0
  224. package/dist/leader/leader.js +151 -4
  225. package/dist/leader/merge.d.ts +94 -0
  226. package/dist/leader/merge.js +112 -9
  227. package/dist/leader/planner.d.ts +52 -0
  228. package/dist/leader/planner.js +47 -3
  229. package/dist/leader/repair-oracle.d.ts +61 -0
  230. package/dist/leader/repair-oracle.js +59 -1
  231. package/dist/leader/repair-wire.d.ts +78 -0
  232. package/dist/leader/repair-wire.js +79 -2
  233. package/dist/leader/wire.d.ts +79 -0
  234. package/dist/leader/wire.js +255 -8
  235. package/dist/lsp/e2b-bridge.d.ts +16 -0
  236. package/dist/lsp/e2b-bridge.js +32 -4
  237. package/dist/lsp/e2b-manager.d.ts +10 -0
  238. package/dist/lsp/e2b-manager.js +18 -2
  239. package/dist/lsp/lsp-frames.d.ts +12 -0
  240. package/dist/lsp/lsp-frames.js +12 -0
  241. package/dist/lsp/manager.d.ts +12 -0
  242. package/dist/lsp/manager.js +35 -2
  243. package/dist/lsp/types.d.ts +10 -0
  244. package/dist/lsp/ws-transport.d.ts +12 -0
  245. package/dist/lsp/ws-transport.js +53 -5
  246. package/dist/lsp-evict.d.ts +14 -0
  247. package/dist/lsp-evict.js +15 -1
  248. package/dist/main.js +530 -2129
  249. package/dist/memory-export.d.ts +1 -0
  250. package/dist/memory-export.js +4 -0
  251. package/dist/memory-scope.d.ts +49 -0
  252. package/dist/memory-scope.js +87 -4
  253. package/dist/memory-sync-client.d.ts +24 -0
  254. package/dist/memory-sync-client.js +41 -2
  255. package/dist/memory-sync.d.ts +23 -0
  256. package/dist/memory-sync.js +70 -1
  257. package/dist/model-select.d.ts +39 -0
  258. package/dist/model-select.js +42 -1
  259. package/dist/observability/cost-quota.d.ts +20 -0
  260. package/dist/observability/cost-quota.js +4 -0
  261. package/dist/observability/cost-taxonomy.d.ts +45 -0
  262. package/dist/observability/cost-taxonomy.js +34 -0
  263. package/dist/observability/logger.d.ts +1 -0
  264. package/dist/observability/logger.js +6 -0
  265. package/dist/observability/metrics.d.ts +13 -0
  266. package/dist/observability/metrics.js +63 -0
  267. package/dist/observability/otel-exporter.d.ts +9 -0
  268. package/dist/observability/otel-exporter.js +11 -3
  269. package/dist/observability/principal-context.d.ts +9 -0
  270. package/dist/observability/principal-context.js +9 -0
  271. package/dist/observability/prompt-manifest.d.ts +37 -0
  272. package/dist/observability/prompt-manifest.js +35 -1
  273. package/dist/observability/rate-limit.d.ts +10 -0
  274. package/dist/observability/rate-limit.js +1 -0
  275. package/dist/observability/tool-trace.d.ts +24 -0
  276. package/dist/observability/tool-trace.js +41 -1
  277. package/dist/orchestration/hardened-vm-runner.d.ts +107 -0
  278. package/dist/orchestration/hardened-vm-runner.js +119 -4
  279. package/dist/orchestration/hardened-vm-worker-runner.d.ts +14 -0
  280. package/dist/orchestration/hardened-vm-worker-runner.js +26 -1
  281. package/dist/orchestration/hardened-vm-worker.js +27 -0
  282. package/dist/orchestration/subagent-steer.d.ts +44 -0
  283. package/dist/orchestration/subagent-steer.js +45 -1
  284. package/dist/orchestration/workflow-agent-steer.d.ts +54 -0
  285. package/dist/orchestration/workflow-agent-steer.js +80 -1
  286. package/dist/orchestration/workflow-completion-inbox.d.ts +189 -2
  287. package/dist/orchestration/workflow-completion-inbox.js +283 -32
  288. package/dist/orchestration/workflow-notify-journal.d.ts +132 -1
  289. package/dist/orchestration/workflow-notify-journal.js +187 -13
  290. package/dist/parked-decide.d.ts +61 -0
  291. package/dist/parked-decide.js +58 -1
  292. package/dist/per-task-image.d.ts +54 -0
  293. package/dist/per-task-image.js +41 -0
  294. package/dist/plan-cache-probe.d.ts +35 -0
  295. package/dist/plan-cache-probe.js +18 -3
  296. package/dist/plugins/approval-exemption-store.d.ts +7 -0
  297. package/dist/plugins/approval-exemption-store.js +28 -2
  298. package/dist/plugins/approval-store-sql.d.ts +64 -0
  299. package/dist/plugins/approval-store-sql.js +32 -0
  300. package/dist/plugins/background-agent-store-sql.d.ts +55 -0
  301. package/dist/plugins/background-agent-store-sql.js +91 -4
  302. package/dist/plugins/background-shell-support.d.ts +98 -0
  303. package/dist/plugins/background-shell-support.js +117 -13
  304. package/dist/plugins/blob-backend.d.ts +104 -0
  305. package/dist/plugins/blob-backend.js +163 -6
  306. package/dist/plugins/breaker-state-sql.d.ts +65 -1
  307. package/dist/plugins/breaker-state-sql.js +46 -8
  308. package/dist/plugins/caching-session-store.d.ts +42 -0
  309. package/dist/plugins/caching-session-store.js +63 -3
  310. package/dist/plugins/checkpoint-store-sql.d.ts +179 -0
  311. package/dist/plugins/checkpoint-store-sql.js +239 -11
  312. package/dist/plugins/file-outcome-sink.d.ts +4 -0
  313. package/dist/plugins/file-outcome-sink.js +9 -0
  314. package/dist/plugins/file-resume-anchor-store.d.ts +20 -0
  315. package/dist/plugins/file-resume-anchor-store.js +43 -4
  316. package/dist/plugins/file-run-store.d.ts +90 -0
  317. package/dist/plugins/file-run-store.js +145 -13
  318. package/dist/plugins/file-snapshot-store-sql.d.ts +133 -1
  319. package/dist/plugins/file-snapshot-store-sql.js +178 -10
  320. package/dist/plugins/file-workflow-journal-store.d.ts +10 -0
  321. package/dist/plugins/file-workflow-journal-store.js +10 -0
  322. package/dist/plugins/fork-routing-session-store.d.ts +47 -0
  323. package/dist/plugins/fork-routing-session-store.js +51 -3
  324. package/dist/plugins/host-platform.d.ts +69 -0
  325. package/dist/plugins/host-platform.js +81 -2
  326. package/dist/plugins/image-bake-store-sql.d.ts +136 -1
  327. package/dist/plugins/image-bake-store-sql.js +241 -7
  328. package/dist/plugins/image-index-sql.d.ts +46 -0
  329. package/dist/plugins/image-index-sql.js +113 -4
  330. package/dist/plugins/k8s-bg-scripts.d.ts +34 -0
  331. package/dist/plugins/k8s-bg-scripts.js +88 -4
  332. package/dist/plugins/k8s-exec-protocol.d.ts +33 -0
  333. package/dist/plugins/k8s-exec-protocol.js +41 -0
  334. package/dist/plugins/local-checkpoint-store.d.ts +40 -0
  335. package/dist/plugins/local-checkpoint-store.js +86 -7
  336. package/dist/plugins/local-session-store.d.ts +157 -0
  337. package/dist/plugins/local-session-store.js +289 -34
  338. package/dist/plugins/local-task-attachment-store.js +14 -2
  339. package/dist/plugins/mailbox-store-sql.d.ts +38 -0
  340. package/dist/plugins/mailbox-store-sql.js +39 -6
  341. package/dist/plugins/memory-engine-pg.d.ts +30 -0
  342. package/dist/plugins/memory-engine-pg.js +122 -4
  343. package/dist/plugins/memory-engine-tidb.d.ts +26 -0
  344. package/dist/plugins/memory-engine-tidb.js +115 -5
  345. package/dist/plugins/memory-engine-vector-util.d.ts +5 -0
  346. package/dist/plugins/memory-engine-vector-util.js +5 -0
  347. package/dist/plugins/memory-resume-anchor-store.d.ts +8 -0
  348. package/dist/plugins/memory-resume-anchor-store.js +17 -0
  349. package/dist/plugins/memory-run-store.d.ts +63 -0
  350. package/dist/plugins/memory-run-store.js +73 -8
  351. package/dist/plugins/memory-session-policy-store.d.ts +17 -0
  352. package/dist/plugins/memory-session-policy-store.js +17 -0
  353. package/dist/plugins/memory-sync-store-pg.d.ts +42 -0
  354. package/dist/plugins/memory-sync-store-pg.js +45 -2
  355. package/dist/plugins/memory-sync-store-tidb.d.ts +9 -0
  356. package/dist/plugins/memory-sync-store-tidb.js +31 -1
  357. package/dist/plugins/outcome-ledger-sql.d.ts +63 -0
  358. package/dist/plugins/outcome-ledger-sql.js +97 -3
  359. package/dist/plugins/pg-approval-store.d.ts +7 -0
  360. package/dist/plugins/pg-approval-store.js +7 -0
  361. package/dist/plugins/pg-breaker-state.d.ts +6 -0
  362. package/dist/plugins/pg-breaker-state.js +6 -0
  363. package/dist/plugins/pg-checkpoint-store.d.ts +8 -0
  364. package/dist/plugins/pg-checkpoint-store.js +8 -0
  365. package/dist/plugins/pg-cost-quota.d.ts +17 -0
  366. package/dist/plugins/pg-cost-quota.js +6 -0
  367. package/dist/plugins/pg-file-snapshot-store.d.ts +6 -0
  368. package/dist/plugins/pg-file-snapshot-store.js +6 -0
  369. package/dist/plugins/pg-image-bake.d.ts +9 -0
  370. package/dist/plugins/pg-image-bake.js +9 -0
  371. package/dist/plugins/pg-image-index.d.ts +9 -0
  372. package/dist/plugins/pg-image-index.js +9 -0
  373. package/dist/plugins/pg-outcome-ledger.d.ts +9 -0
  374. package/dist/plugins/pg-outcome-ledger.js +9 -0
  375. package/dist/plugins/pg-pool.d.ts +50 -0
  376. package/dist/plugins/pg-pool.js +78 -0
  377. package/dist/plugins/pg-query.d.ts +11 -0
  378. package/dist/plugins/pg-rate-limiter.d.ts +23 -0
  379. package/dist/plugins/pg-rate-limiter.js +12 -2
  380. package/dist/plugins/pg-resume-anchor-store.d.ts +5 -0
  381. package/dist/plugins/pg-resume-anchor-store.js +5 -0
  382. package/dist/plugins/pg-run-store.d.ts +7 -0
  383. package/dist/plugins/pg-run-store.js +7 -0
  384. package/dist/plugins/pg-safe-json.d.ts +13 -0
  385. package/dist/plugins/pg-safe-json.js +40 -4
  386. package/dist/plugins/pg-session-policy-store.d.ts +5 -0
  387. package/dist/plugins/pg-session-policy-store.js +5 -0
  388. package/dist/plugins/pg-session-storage.d.ts +72 -0
  389. package/dist/plugins/pg-session-storage.js +152 -20
  390. package/dist/plugins/pg-session-store.d.ts +10 -0
  391. package/dist/plugins/pg-session-store.js +10 -0
  392. package/dist/plugins/pg-tool-result-store.d.ts +7 -0
  393. package/dist/plugins/pg-tool-result-store.js +7 -0
  394. package/dist/plugins/pg-workflow-journal-store.d.ts +7 -0
  395. package/dist/plugins/pg-workflow-journal-store.js +7 -0
  396. package/dist/plugins/pg-workflow-run-store.d.ts +7 -0
  397. package/dist/plugins/pg-workflow-run-store.js +7 -0
  398. package/dist/plugins/posix-shell-fs.d.ts +3 -0
  399. package/dist/plugins/posix-shell-fs.js +31 -1
  400. package/dist/plugins/remote-env-adb.d.ts +25 -0
  401. package/dist/plugins/remote-env-adb.js +95 -12
  402. package/dist/plugins/remote-env-e2b.d.ts +146 -0
  403. package/dist/plugins/remote-env-e2b.js +371 -40
  404. package/dist/plugins/remote-env-file-error.d.ts +34 -0
  405. package/dist/plugins/remote-env-file-error.js +37 -0
  406. package/dist/plugins/remote-env-host.d.ts +151 -0
  407. package/dist/plugins/remote-env-host.js +468 -70
  408. package/dist/plugins/remote-env-k8s.d.ts +135 -0
  409. package/dist/plugins/remote-env-k8s.js +318 -32
  410. package/dist/plugins/remote-env-local-docker.d.ts +65 -0
  411. package/dist/plugins/remote-env-local-docker.js +149 -24
  412. package/dist/plugins/remote-env-ssh.d.ts +35 -0
  413. package/dist/plugins/remote-env-ssh.js +117 -20
  414. package/dist/plugins/remote-scratchpad.d.ts +38 -0
  415. package/dist/plugins/remote-scratchpad.js +32 -2
  416. package/dist/plugins/remote-shell.d.ts +22 -0
  417. package/dist/plugins/remote-shell.js +27 -1
  418. package/dist/plugins/resume-anchor-store-sql.d.ts +28 -0
  419. package/dist/plugins/resume-anchor-store-sql.js +11 -0
  420. package/dist/plugins/roster-store-sql.d.ts +27 -0
  421. package/dist/plugins/roster-store-sql.js +54 -0
  422. package/dist/plugins/run-store-sql.d.ts +194 -0
  423. package/dist/plugins/run-store-sql.js +210 -9
  424. package/dist/plugins/s3-presign.d.ts +20 -0
  425. package/dist/plugins/s3-presign.js +49 -2
  426. package/dist/plugins/scheduler-support.d.ts +12 -0
  427. package/dist/plugins/scheduler-support.js +75 -3
  428. package/dist/plugins/send-file-ledger.d.ts +15 -0
  429. package/dist/plugins/send-file-ledger.js +53 -4
  430. package/dist/plugins/send-user-file.d.ts +77 -0
  431. package/dist/plugins/send-user-file.js +92 -4
  432. package/dist/plugins/session-policy-store-sql.d.ts +15 -0
  433. package/dist/plugins/session-policy-store-sql.js +75 -3
  434. package/dist/plugins/session-store.d.ts +45 -0
  435. package/dist/plugins/session-store.js +51 -0
  436. package/dist/plugins/sql-driver.d.ts +77 -0
  437. package/dist/plugins/sql-driver.js +4 -0
  438. package/dist/plugins/sql-escape.d.ts +8 -0
  439. package/dist/plugins/sql-escape.js +8 -0
  440. package/dist/plugins/sql-row-helpers.d.ts +25 -0
  441. package/dist/plugins/sql-row-helpers.js +25 -0
  442. package/dist/plugins/store-backend.d.ts +103 -0
  443. package/dist/plugins/store-backend.js +120 -27
  444. package/dist/plugins/store-contracts.d.ts +62 -0
  445. package/dist/plugins/store-contracts.js +3 -0
  446. package/dist/plugins/task-attachment-store.d.ts +60 -0
  447. package/dist/plugins/task-attachment-store.js +49 -3
  448. package/dist/plugins/task-list-store-sql.d.ts +25 -0
  449. package/dist/plugins/task-list-store-sql.js +21 -0
  450. package/dist/plugins/tidb-approval-store.d.ts +6 -0
  451. package/dist/plugins/tidb-approval-store.js +6 -0
  452. package/dist/plugins/tidb-breaker-state.d.ts +5 -0
  453. package/dist/plugins/tidb-breaker-state.js +5 -0
  454. package/dist/plugins/tidb-checkpoint-store.d.ts +7 -0
  455. package/dist/plugins/tidb-checkpoint-store.js +7 -0
  456. package/dist/plugins/tidb-cost-quota.d.ts +6 -0
  457. package/dist/plugins/tidb-cost-quota.js +1 -0
  458. package/dist/plugins/tidb-file-snapshot-store.d.ts +6 -0
  459. package/dist/plugins/tidb-file-snapshot-store.js +6 -0
  460. package/dist/plugins/tidb-image-bake.d.ts +8 -0
  461. package/dist/plugins/tidb-image-bake.js +8 -0
  462. package/dist/plugins/tidb-image-index.d.ts +8 -0
  463. package/dist/plugins/tidb-image-index.js +8 -0
  464. package/dist/plugins/tidb-outcome-ledger.d.ts +10 -0
  465. package/dist/plugins/tidb-outcome-ledger.js +10 -0
  466. package/dist/plugins/tidb-pool.d.ts +56 -0
  467. package/dist/plugins/tidb-pool.js +181 -1
  468. package/dist/plugins/tidb-rate-limiter.d.ts +11 -0
  469. package/dist/plugins/tidb-rate-limiter.js +5 -2
  470. package/dist/plugins/tidb-resume-anchor-store.d.ts +5 -0
  471. package/dist/plugins/tidb-resume-anchor-store.js +5 -0
  472. package/dist/plugins/tidb-run-store.d.ts +7 -0
  473. package/dist/plugins/tidb-run-store.js +7 -0
  474. package/dist/plugins/tidb-session-policy-store.d.ts +5 -0
  475. package/dist/plugins/tidb-session-policy-store.js +5 -0
  476. package/dist/plugins/tidb-session-storage.d.ts +43 -0
  477. package/dist/plugins/tidb-session-storage.js +62 -3
  478. package/dist/plugins/tidb-session-store.d.ts +142 -0
  479. package/dist/plugins/tidb-session-store.js +264 -16
  480. package/dist/plugins/tidb-tool-result-store.d.ts +5 -0
  481. package/dist/plugins/tidb-tool-result-store.js +7 -0
  482. package/dist/plugins/tidb-workflow-journal-store.d.ts +7 -0
  483. package/dist/plugins/tidb-workflow-journal-store.js +7 -0
  484. package/dist/plugins/tidb-workflow-run-store.d.ts +8 -0
  485. package/dist/plugins/tidb-workflow-run-store.js +8 -0
  486. package/dist/plugins/tool-result-store-sql.d.ts +58 -0
  487. package/dist/plugins/tool-result-store-sql.js +38 -2
  488. package/dist/plugins/web-search.d.ts +35 -0
  489. package/dist/plugins/web-search.js +45 -9
  490. package/dist/plugins/workflow-journal-limits.d.ts +10 -0
  491. package/dist/plugins/workflow-journal-limits.js +10 -0
  492. package/dist/plugins/workflow-journal-store-sql.d.ts +78 -0
  493. package/dist/plugins/workflow-journal-store-sql.js +58 -2
  494. package/dist/plugins/workflow-run-store-sql.d.ts +84 -0
  495. package/dist/plugins/workflow-run-store-sql.js +84 -6
  496. package/dist/plugins/worktree-isolation.d.ts +45 -0
  497. package/dist/plugins/worktree-isolation.js +105 -6
  498. package/dist/plugins/write-behind-counter.d.ts +56 -1
  499. package/dist/plugins/write-behind-counter.js +53 -14
  500. package/dist/principal-jwt.d.ts +24 -0
  501. package/dist/principal-jwt.js +60 -5
  502. package/dist/project-memory.d.ts +22 -0
  503. package/dist/project-memory.js +144 -15
  504. package/dist/prompts-domain-validate.d.ts +28 -0
  505. package/dist/prompts-domain-validate.js +42 -1
  506. package/dist/question.d.ts +33 -0
  507. package/dist/question.js +69 -2
  508. package/dist/resource-suspend.d.ts +21 -0
  509. package/dist/resource-suspend.js +16 -0
  510. package/dist/router/route-orchestration.d.ts +78 -0
  511. package/dist/router/route-orchestration.js +77 -0
  512. package/dist/run-local.d.ts +32 -0
  513. package/dist/run-local.js +172 -8
  514. package/dist/runs.d.ts +189 -4
  515. package/dist/runs.js +465 -24
  516. package/dist/runtime-caps-resolver.d.ts +106 -0
  517. package/dist/runtime-caps-resolver.js +89 -4
  518. package/dist/runtime-governance.d.ts +84 -0
  519. package/dist/runtime-governance.js +127 -1
  520. package/dist/sandbox-pkg-source.d.ts +23 -0
  521. package/dist/sandbox-pkg-source.js +37 -0
  522. package/dist/sealed-key.d.ts +54 -0
  523. package/dist/sealed-key.js +68 -3
  524. package/dist/security.d.ts +108 -0
  525. package/dist/security.js +145 -6
  526. package/dist/sema-registry.d.ts +21 -0
  527. package/dist/sema-registry.js +21 -0
  528. package/dist/session-leaf-bus.d.ts +31 -0
  529. package/dist/session-leaf-bus.js +32 -0
  530. package/dist/session-sync-content.d.ts +46 -0
  531. package/dist/session-sync-content.js +47 -1
  532. package/dist/session-sync-kernel.d.ts +89 -0
  533. package/dist/session-sync-kernel.js +48 -3
  534. package/dist/session-sync.d.ts +125 -0
  535. package/dist/session-sync.js +181 -8
  536. package/dist/session-titler.d.ts +26 -0
  537. package/dist/session-titler.js +41 -7
  538. package/dist/session-watch.d.ts +53 -1
  539. package/dist/session-watch.js +73 -9
  540. package/dist/sighup-idle.d.ts +30 -0
  541. package/dist/sighup-idle.js +12 -1
  542. package/dist/spec-fields.d.ts +91 -3
  543. package/dist/spec-fields.js +116 -4
  544. package/dist/task-cwd.d.ts +50 -0
  545. package/dist/task-cwd.js +68 -1
  546. package/dist/task-mcp.d.ts +47 -0
  547. package/dist/task-mcp.js +29 -0
  548. package/dist/task-settings.d.ts +175 -0
  549. package/dist/task-settings.js +226 -11
  550. package/dist/task-workflow.d.ts +67 -0
  551. package/dist/task-workflow.js +75 -8
  552. package/dist/tool-approval.d.ts +91 -0
  553. package/dist/tool-approval.js +215 -0
  554. package/dist/trace/artifacts.d.ts +9 -0
  555. package/dist/trace/artifacts.js +30 -5
  556. package/dist/trace/core-keyset-guard.d.ts +18 -0
  557. package/dist/trace/ledger-sink.d.ts +43 -0
  558. package/dist/trace/ledger-sink.js +64 -5
  559. package/dist/trace/project.d.ts +133 -0
  560. package/dist/trace/project.js +189 -2
  561. package/dist/trace/redact.d.ts +19 -0
  562. package/dist/trace/redact.js +66 -9
  563. package/dist/usage-analytics.d.ts +26 -0
  564. package/dist/usage-analytics.js +26 -3
  565. package/package.json +1 -1
@@ -1,3 +1,24 @@
1
+ /**
2
+ * `RemoteContainerExecutionEnv` — the E2B implementation of core's `RemoteExecutionEnv` seam
3
+ * (core src/core/remote-env.ts, design/48 §5). The service control plane owns this backend; core
4
+ * owns the seam shape (exactly like `CheckpointStore` / `SqlExecutor`).
5
+ *
6
+ * **Scope = design/48 §0.5 v1 minimal slice**: single worker, E2B managed, **Tier 2 only** (every
7
+ * op runs in a per-task E2B Firecracker VM — no tiering/classification), and the ~4 live seam methods
8
+ * `connect` / `execStream` / `workspaceHandle` / `destroy` + the base `ExecutionEnv` (FileSystem + Shell).
9
+ *
10
+ * **v1.5 (design/49, core 1.69):** `suspendVM` (E2B `pause()`) / `resumeVM` (`Sandbox.connect(id)` auto-resume)
11
+ * / `postResumeInit` (reachability + at-rest-encryption red line) are IMPLEMENTED — core orchestrates them when
12
+ * a task with this remote env hits a durable-approval `ask` (workspaceHandle → Checkpoint.state). `reconnect`
13
+ * (cross-replica re-attach to a still-running VM) stays deferred to **v2** (v1.5 always snapshots → resumeVM).
14
+ *
15
+ * **Provider seam discipline (self-host endgame):** every E2B SDK call is confined to this one class. A
16
+ * future OSS-self-hosted E2B (clay's "最终自部署" constraint) swaps the backend without touching consumers.
17
+ *
18
+ * **never-throw invariant:** the base `ExecutionEnv` contract requires every FileSystem/Shell method to
19
+ * encode failure in the returned `Result` (never throw). The one exception is `execStream`, whose async
20
+ * iterator MAY throw `RemoteExecutionError` mid-stream before an `exit` chunk (seam contract, OutputChunk).
21
+ */
1
22
  import path from "node:path";
2
23
  import { Sandbox, CommandExitError, FileType } from "e2b";
3
24
  import { FileError, ExecutionError, RemoteExecutionError, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
@@ -5,32 +26,91 @@ import { fileErrorFromExec } from "./remote-env-file-error.js";
5
26
  import { numEnvOr } from "./remote-shell.js";
6
27
  import { BackgroundShellManager } from "./background-shell-support.js";
7
28
  const PROVIDER = "e2b";
29
+ /** E2B default sandbox user home — the workspace root unless overridden. */
8
30
  const DEFAULT_MOUNT_PATH = "/home/user";
31
+ /** Default sandbox lifetime if the caller does not set one (E2B bills for this window). */
9
32
  const DEFAULT_TIMEOUT_MS = 5 * 60_000;
33
+ /**
34
+ * 🔴 BL-31 — the E2B sandbox lifetime (`Sandbox.create({ timeoutMs })`) is bounded by E2B's per-sandbox MAX
35
+ * (≈1h on the fleet plan). A caller that passes a long deadline as the sandbox lifetime — the static-lane leader
36
+ * forwards `leaderTimeoutMs` (default 24h, the leader's OVERALL deadline) straight through — makes `create()`
37
+ * reject and `connect()` fail (found live: canary leader judare, 2026-06-17). Clamp the requested lifetime to
38
+ * this max (env-tunable `E2B_SANDBOX_MAX_MS`, default 1h) — the sandbox lifetime is decoupled from the leader's
39
+ * overall deadline. A run longer than the clamp needs a sandbox keep-alive/extend (`setTimeout`, follow-up);
40
+ * today an unclamped 24h simply never connects, so a clamp is strictly safer.
41
+ */
10
42
  const E2B_SANDBOX_MAX_MS = numEnvOr("E2B_SANDBOX_MAX_MS", 3_600_000, 60_000);
43
+ /**
44
+ * 🔴 Class-fix for the E2B-hang bug (core timeout/liveness contract). Three SEMANTICALLY DISTINCT
45
+ * timeouts that were previously conflated into the one sandbox-lifetime value:
46
+ * - {@link DEFAULT_RPC_TIMEOUT_MS} — wall-clock bound for a CONTROL-PLANE RPC (create/resume/pause/kill/isRunning/
47
+ * files.*). These are "should-be-fast" ops; a provider hang here used to wait the whole lifetime (~30 min).
48
+ * - {@link DEFAULT_LIVENESS_MS} — idle/liveness bound for a COMMAND (exec/execStream): max time with NO output
49
+ * AND not yet complete = suspected hang. The precise hang-killer: only kills "zero-progress",
50
+ * never a slow-but-streaming `npm ci`. MUST also cover the command-CREATION RPC (hang happens before chunk #1).
51
+ * - per-command wall-clock = the caller's explicit `timeout` (slow-but-progressing cap), which MUST NOT fall back
52
+ * to the sandbox lifetime. Unset → liveness is the safety net, not the lifetime.
53
+ */
11
54
  const DEFAULT_RPC_TIMEOUT_MS = 60_000;
12
55
  const DEFAULT_LIVENESS_MS = 120_000;
56
+ /**
57
+ * Wall-clock for DATA-TRANSFER / provisioning RPCs (large `files.read`/`files.write`, `suspendVM`/`resumeVM`
58
+ * snapshot, sandbox `create`/`connect`). 🔴 These legitimately run MINUTES — a short fixed
59
+ * wall-clock would kill the durable-checkpoint path (suspending a large workspace). E2B's file/lifecycle SDK
60
+ * exposes NO progress/heartbeat to drive a true idle-bound, so this is a GENEROUS wall-clock; the moment the SDK
61
+ * surfaces progress, these should move to a real liveness bound. OP-CLASS sized: cheap control RPCs
62
+ * (isRunning/kill/metadata) keep the short {@link DEFAULT_RPC_TIMEOUT_MS}; this only widens the transfer class.
63
+ */
13
64
  const DEFAULT_DATA_TIMEOUT_MS = 5 * 60_000;
65
+ /**
66
+ * design/103 background-shell bounds for E2B (TOB). A background process cannot outlive its sandbox, so the BG
67
+ * timeout ceiling is well under the per-sandbox max ({@link E2B_SANDBOX_MAX_MS}). `maxConcurrent` mirrors the TOC
68
+ * default (8) — anti fork-bomb, not a workload limit.
69
+ */
14
70
  const E2B_BG_MAX_CONCURRENT = numEnvOr("E2B_BG_MAX_CONCURRENT", 8, 1);
15
71
  const E2B_BG_DEFAULT_TIMEOUT_SEC = numEnvOr("E2B_BG_DEFAULT_TIMEOUT_SEC", 300, 1);
16
72
  const E2B_BG_MAX_TIMEOUT_SEC = numEnvOr("E2B_BG_MAX_TIMEOUT_SEC", 1800, E2B_BG_DEFAULT_TIMEOUT_SEC);
73
+ /** Pad (sec) added to the E2B command `timeoutMs` backstop so the control-plane hard wall flips status to `killed` FIRST. */
17
74
  const E2B_BG_PROVIDER_BACKSTOP_PAD_SEC = 30;
18
75
  const ok = (value) => ({ ok: true, value });
76
+ /**
77
+ * E2B-backed remote `ExecutionEnv`. **Lazy by default (v1b):** the VM is provisioned on the
78
+ * FIRST FileSystem/Shell/execStream call (auto `connect()`), so a task that never touches the hand pays zero
79
+ * cost. `connect()` may still be called explicitly (idempotent + concurrency-memoized) if the control plane
80
+ * wants to provision eagerly. `destroy()` on task end (the Runner calls it via `hasDestroy`); a never-used env's
81
+ * destroy is a no-op (nothing to kill, no bill). One container per task (design/48 §5 §7 Q7).
82
+ */
19
83
  export class RemoteContainerExecutionEnv {
84
+ /** Current working directory for relative paths (FileSystem contract; mutable, core may reassign). */
20
85
  cwd;
21
86
  cfg;
22
87
  sandboxApi;
23
88
  sbx;
89
+ /** 🔴 BL-31 keep-alive — the E2B sandbox lifetime is hard-capped (≈1h, {@link E2B_SANDBOX_MAX_MS}), but a long
90
+ * leader/repair run legitimately exceeds it. This interval RE-extends the sandbox (`setTimeout(timeoutMs)` resets
91
+ * the auto-kill window from now) while the env is held, so the sandbox tracks ACTUAL usage rather than dying
92
+ * mid-run at the cap (otherwise: a silent dead sandbox at hour 1). Cleared on destroy;
93
+ * a no-op while paused/destroyed. `unref`'d so it never holds the process open. */
24
94
  keepAlive;
25
95
  handle;
96
+ /** Terminal once set by {@link destroy} — never reset to false (no resurrection). Guards connect/ensureConnected
97
+ * and the post-await re-check in BOTH async-provision paths ({@link doConnect} and {@link doResumeVM}) against
98
+ * a destroy that races an in-flight provision/restore. */
26
99
  destroyed = false;
100
+ /** In-flight connect/resume, memoized so concurrent lazy first-use OR resume provisions exactly one VM (no double-bill). */
27
101
  connecting;
102
+ /** Count of exec/execStream calls in flight — `suspendVM` refuses while > 0 (council #4: never snapshot a running command). */
28
103
  activeCommands = 0;
104
+ /** True between a `suspendVM` (VM paused, external connection dead) and the next `resumeVM` reconnect. While
105
+ * paused, `this.sbx` still points at the (now-dead) handle, so `resumeVM`'s idempotency guard must NOT treat
106
+ * it as live — it has to fall through and actually `Sandbox.connect()`. Without this, a suspend→resume on the
107
+ * SAME env instance skips the reconnect and every later fs/exec runs against the dead VM. */
29
108
  paused = false;
30
109
  constructor(config, deps = {}) {
31
110
  this.cfg = {
32
111
  ...config,
33
112
  mountPath: config.mountPath ?? DEFAULT_MOUNT_PATH,
113
+ // BL-31: clamp the sandbox lifetime to E2B's per-sandbox max (a 24h leaderTimeoutMs blows the cap → connect fails).
34
114
  timeoutMs: Math.min(config.timeoutMs ?? DEFAULT_TIMEOUT_MS, E2B_SANDBOX_MAX_MS),
35
115
  allowInternetAccess: config.allowInternetAccess ?? true,
36
116
  rpcTimeoutMs: config.rpcTimeoutMs ?? DEFAULT_RPC_TIMEOUT_MS,
@@ -40,17 +120,30 @@ export class RemoteContainerExecutionEnv {
40
120
  this.cwd = this.cfg.mountPath;
41
121
  this.sandboxApi = deps.sandboxApi ?? Sandbox;
42
122
  }
123
+ /** design/61 §9: E2B is a FULL RemoteExecutionEnv — an isolated, snapshot-suspendable microVM. (SSH/ADB
124
+ * adapters declare {isolation:false, suspendable:false} — real machines/devices, not snapshotable.) */
43
125
  capabilities = { isolation: true, suspendable: true };
126
+ // ─────────────────────────────── RemoteExecutionEnv: lifecycle ───────────────────────────────
44
127
  workspaceHandle() {
45
128
  if (!this.handle) {
46
129
  throw new RemoteExecutionError("connect_failed", "workspaceHandle() called before connect()");
47
130
  }
48
131
  return this.handle;
49
132
  }
133
+ /**
134
+ * Public host for a port inside the sandbox (E2B reverse proxy → `<port>-<id>.e2b.app`). The LSP sidecar
135
+ * (design/64 §13.1) uses this to reach the in-sandbox language-server WS bridge over `wss://`. Lazily
136
+ * provisions the VM (like exec/fs); `undefined` if it can't connect → the LSP manager degrades.
137
+ */
50
138
  async getHost(port) {
51
139
  const conn = await this.ensureConnected();
52
140
  return conn.ok ? conn.value.getHost(port) : undefined;
53
141
  }
142
+ /**
143
+ * Launch a long-running process as a TRUE E2B background command (resolves once launched, E2B keeps it alive)
144
+ * — for the LSP bridge (design/64 §13.1). NOT the same as `exec` with a shell `&`: E2B reaps a foreground
145
+ * command's process group on completion, which would kill an in-shell `&`/nohup child. Fire-and-forget.
146
+ */
54
147
  async startBackground(command) {
55
148
  const conn = await this.ensureConnected();
56
149
  if (conn.ok)
@@ -58,46 +151,57 @@ export class RemoteContainerExecutionEnv {
58
151
  }
59
152
  async connect(config) {
60
153
  if (config?.snapshotId) {
154
+ // resume-from-snapshot lands via resumeVM (v1.5); connect() just delegates.
61
155
  return this.resumeVM(config.snapshotId, config.abortSignal ? { abortSignal: config.abortSignal } : undefined);
62
156
  }
63
157
  if (this.destroyed)
64
158
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env already destroyed") };
159
+ // A paused (suspended) env still HOLDS this.sbx but the external connection is dead — returning it as live
160
+ // would let a late fs/exec hit the dead VM with a confusing low-level error. Fail clearly until resumeVM.
65
161
  if (this.paused)
66
162
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env is suspended (paused) — resumeVM(snapshotId) before use") };
67
163
  if (this.sbx)
68
- return ok(this.workspaceHandle());
164
+ return ok(this.workspaceHandle()); // idempotent: already connected
69
165
  if (this.connecting)
70
- return this.connecting;
166
+ return this.connecting; // concurrent first-use shares one provision (lazy path)
71
167
  this.connecting = this.doConnect(config);
72
168
  const r = await this.connecting;
73
169
  if (!r.ok)
74
- this.connecting = undefined;
170
+ this.connecting = undefined; // failed connect → allow a later retry
75
171
  return r;
76
172
  }
77
173
  async doConnect(config) {
78
174
  try {
175
+ // ⚠️ This `await` yields — destroy() can run during it. A destroy that lands here sees sbx===undefined
176
+ // and returns without killing; if we then blindly assigned + reset `destroyed`, we'd RESURRECT a
177
+ // destroyed env and orphan a live billable VM (the Runner never calls destroy() twice). So after the
178
+ // await we re-check `destroyed` and kill-then-bail. Note: destroy is terminal — we never reset it to false.
79
179
  const sbx = await this.rpc("sandbox.create", () => this.sandboxApi.create({
80
180
  ...(this.cfg.template ? { template: this.cfg.template } : {}),
81
181
  apiKey: this.cfg.apiKey,
82
182
  timeoutMs: this.cfg.timeoutMs,
83
183
  allowInternetAccess: this.cfg.allowInternetAccess,
84
184
  ...(this.cfg.metadata ? { metadata: this.cfg.metadata } : {}),
185
+ // Out-of-band secret/config env (never logged) — sandbox-wide so every command + the LSP/bg process
186
+ // sees it without the value ever touching the model prompt or the tool command string.
85
187
  ...(this.cfg.sandboxEnv && Object.keys(this.cfg.sandboxEnv).length > 0 ? { envs: this.cfg.sandboxEnv } : {}),
86
188
  }), config?.abortSignal, this.cfg.dataTimeoutMs);
87
189
  if (this.destroyed) {
88
- await sbx.kill().catch(() => { });
190
+ await sbx.kill().catch(() => { }); // raced with destroy() → kill the just-provisioned VM (no leak)
89
191
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env destroyed during connect") };
90
192
  }
91
193
  this.sbx = sbx;
92
194
  this.paused = false;
93
- this.startKeepAlive();
195
+ this.startKeepAlive(); // BL-31 — keep the sandbox alive past its ≤1h lifetime cap while the env is held.
94
196
  this.cfg.logger?.info?.("remote_env_connect", { sandboxId: sbx.sandboxId });
95
197
  this.handle = {
96
198
  sandboxId: sbx.sandboxId,
97
199
  provider: PROVIDER,
98
200
  mountPath: this.cfg.mountPath,
99
- sessionToken: sbx.sandboxId,
201
+ sessionToken: sbx.sandboxId, // a still-running E2B VM is re-attachable by its id (Sandbox.connect)
100
202
  };
203
+ // design/48 §5 gap 7: secrets are injected at connect time. v1 defers secret *resolution* to the
204
+ // control plane (SecretRef carries only name+locator, no value) — recorded, wired in v1b/v1.5.
101
205
  if (config?.secrets?.length) {
102
206
  this.cfg.logger?.warn?.("remote-env: connect secrets are not injected in v1 (control-plane resolution deferred)", {
103
207
  count: config.secrets.length,
@@ -111,6 +215,12 @@ export class RemoteContainerExecutionEnv {
111
215
  return { ok: false, error: this.remoteError(e, "connect_failed") };
112
216
  }
113
217
  }
218
+ /**
219
+ * v1.5 (design/49): snapshot the workspace via E2B `pause()` (stores fs+memory+process; the snapshot id IS
220
+ * the sandbox id — a paused E2B sandbox is resumed by its id). Atomic contract (council): on failure the VM
221
+ * is left UNTOUCHED (still running) so core can fall back to onAsk. Refuses if a command is in flight
222
+ * (council #4) — never silently snapshot a running command. `abortSignal` bounds a hung pause (council BUG#2).
223
+ */
114
224
  async suspendVM(options) {
115
225
  const sbx = this.sbx;
116
226
  if (!sbx)
@@ -122,12 +232,18 @@ export class RemoteContainerExecutionEnv {
122
232
  }
123
233
  try {
124
234
  await this.rpc("sandbox.pause", (signal) => sbx.pause({ signal }), options?.abortSignal, this.cfg.dataTimeoutMs);
235
+ // Same race as doConnect/doResumeVM: this `await` yielded, so destroy() may have landed. If destroy()
236
+ // killed a still-pausing sandbox its kill can be rejected, leaving a paused-but-unreferenced snapshot
237
+ // (billed storage until E2B's TTL); and we must NOT report a snapshotId for it — core would persist it
238
+ // and every later resume would fail. Best-effort kill (symmetric with doConnect/doResumeVM) + bail.
125
239
  if (this.destroyed) {
240
+ // best-effort: if BOTH this and destroy()'s kill fail, a paused snapshot is orphaned (billed storage)
241
+ // until E2B's TTL — log it (destroy() logs its own kill failures too) so that is observable.
126
242
  await sbx.kill().catch((e) => this.cfg.logger?.warn?.("remote-env: suspend-recheck kill failed (snapshot may orphan)", { error: errMsg(e) }));
127
243
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env destroyed during suspend") };
128
244
  }
129
- this.paused = true;
130
- this.connecting = undefined;
245
+ this.paused = true; // VM paused → external connection is dead; force resumeVM to actually reconnect
246
+ this.connecting = undefined; // drop the now-resolved prior provision promise so resumeVM doesn't return it instead of reconnecting
131
247
  this.cfg.metrics?.inc("remote_env_suspend_total", { provider: PROVIDER, result: "ok" });
132
248
  this.cfg.logger?.info?.("remote_env_suspend", { snapshotId: sbx.sandboxId });
133
249
  return ok(sbx.sandboxId);
@@ -137,30 +253,48 @@ export class RemoteContainerExecutionEnv {
137
253
  return { ok: false, error: this.remoteError(e, options?.abortSignal?.aborted ? "aborted" : "unknown") };
138
254
  }
139
255
  }
256
+ /**
257
+ * v1.5: restore from a snapshot via E2B `Sandbox.connect(snapshotId)` (auto-resumes a paused sandbox).
258
+ * Idempotent: a no-op if already on that sandbox. Concurrent resume/connect for the same snapshot share ONE
259
+ * provision (memoized via `connecting`, like {@link connect}) so a racing call never double-creates a billable
260
+ * VM (council #3). `abortSignal` bounds a hung restore.
261
+ */
140
262
  async resumeVM(snapshotId, options) {
141
263
  if (this.destroyed)
142
264
  return { ok: false, error: new RemoteExecutionError("connect_failed", "env already destroyed") };
143
265
  if (this.sbx && !this.paused && this.sbx.sandboxId === snapshotId)
144
- return ok(this.workspaceHandle());
266
+ return ok(this.workspaceHandle()); // already resumed + live (idempotent); a paused sbx must reconnect
145
267
  if (this.connecting)
146
- return this.connecting;
268
+ return this.connecting; // concurrent resume/connect shares one provision (council #3)
147
269
  this.connecting = this.doResumeVM(snapshotId, options);
148
270
  const r = await this.connecting;
149
271
  if (!r.ok)
150
- this.connecting = undefined;
272
+ this.connecting = undefined; // failed resume → allow a later retry
151
273
  return r;
152
274
  }
153
275
  async doResumeVM(snapshotId, options) {
154
276
  try {
277
+ // Carry the SAME apiKey + lifetime as doConnect. `SandboxConnectOpts.timeoutMs` defaults to 5 minutes;
278
+ // omitting it would reset a resumed VM's lifetime to 5min regardless of cfg.timeoutMs, so a long task
279
+ // resumed after a durable-approval ask would be reaped by E2B mid-run (snapshot already consumed). The
280
+ // apiKey is explicit too (don't depend on the SDK's E2B_API_KEY env fallback once keys move to the
281
+ // control plane). Wrapped in `rpc` so the connect also gets the data-transfer timeout/abort.
155
282
  const sbx = await this.rpc("sandbox.connect", (signal) => this.sandboxApi.connect(snapshotId, { apiKey: this.cfg.apiKey, timeoutMs: this.cfg.timeoutMs, signal }), options?.abortSignal, this.cfg.dataTimeoutMs);
283
+ // Same race as doConnect: this `await` yields → destroy() can land during it. Re-check + kill-then-bail
284
+ // so we never assign a live VM to a destroyed env (which would orphan a billable sandbox — destroy() is
285
+ // terminal and won't be called again). (council #1)
156
286
  if (this.destroyed) {
157
287
  await sbx.kill().catch(() => { });
158
288
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env destroyed during resume") };
159
289
  }
160
290
  this.sbx = sbx;
161
291
  this.paused = false;
162
- this.startKeepAlive();
292
+ this.startKeepAlive(); // BL-31 — re-arm the keep-alive on a resumed sandbox too.
163
293
  this.cfg.logger?.info?.("remote_env_resume", { requested: snapshotId, connected: sbx.sandboxId });
294
+ // [1467]①(core 1.360 VmLifecycleOptions.priorHandle):恢复根=checkpoint 的 mountPath——E2B resume
295
+ // 恢复的是整个 VM 文件系统,旧根里的产物原位在;当前配置值只该管新建沙箱。exec/fs/mktemp 三腿同源
296
+ // 消费 this.cwd,一并重基;handle 回报的就是真生效根(诚实契约:绝不回显未兑现值)。缺 priorHandle
297
+ // (旧 core/旧 checkpoint)= 落当前配置值——此窗内改 E2B_MOUNT_PATH 仍需先 drain 挂起任务。
164
298
  const restoredRoot = options?.priorHandle?.mountPath ?? this.cfg.mountPath;
165
299
  this.cwd = restoredRoot;
166
300
  this.handle = { sandboxId: sbx.sandboxId, provider: PROVIDER, mountPath: restoredRoot, snapshotId, sessionToken: sbx.sandboxId };
@@ -172,9 +306,17 @@ export class RemoteContainerExecutionEnv {
172
306
  return { ok: false, error: this.remoteError(e, options?.abortSignal?.aborted ? "aborted" : "connect_failed") };
173
307
  }
174
308
  }
309
+ /** Cross-replica re-attach to a still-running VM = v2 (design/49 council #15: v1.5 always snapshots→resumeVM). */
175
310
  async reconnect(_sessionToken) {
176
311
  return { ok: false, error: new RemoteExecutionError("unknown", "reconnect is deferred to v2 (v1.5 always snapshots → use resumeVM)") };
177
312
  }
313
+ /**
314
+ * v1.5: re-establish consistency after resumeVM. The resumed VM is restored to its paused state (fs+memory+
315
+ * process); this verifies it is reachable. Per-command secrets are re-applied automatically (this adapter
316
+ * injects envs per command, not persistently). The VM-internal agent rebuilds its own external connections
317
+ * (git/API don't survive pause). 🔴 At-rest snapshot encryption is the provider's here (E2B managed) — a
318
+ * self-hosted E2B (v2) MUST guarantee it before any secret injection (remote-env.ts §5 red line).
319
+ */
178
320
  async postResumeInit() {
179
321
  const sbx = this.sbx;
180
322
  if (!sbx)
@@ -189,12 +331,15 @@ export class RemoteContainerExecutionEnv {
189
331
  return { ok: false, error: this.remoteError(e, "post_resume_failed") };
190
332
  }
191
333
  }
334
+ /** BL-31 — (re)start the periodic sandbox-lifetime extension. `setTimeout(timeoutMs)` resets the auto-kill window
335
+ * from now; we re-arm at ~80% of the (clamped, ≤1h) window so a long-held sandbox never expires mid-run. A no-op
336
+ * when paused/destroyed; `unref`'d so it never keeps the process alive; cleared on destroy. */
192
337
  startKeepAlive() {
193
338
  this.stopKeepAlive();
194
339
  const everyMs = Math.max(30_000, Math.floor(this.cfg.timeoutMs * 0.8));
195
340
  this.keepAlive = setInterval(() => {
196
341
  if (this.destroyed || this.paused || !this.sbx)
197
- return;
342
+ return; // paused/dead → the resume path re-arms it
198
343
  void this.sbx.setTimeout(this.cfg.timeoutMs).catch((e) => this.cfg.logger?.warn?.("remote_env_keepalive_failed", { error: errMsg(e) }));
199
344
  }, everyMs);
200
345
  this.keepAlive.unref?.();
@@ -207,37 +352,47 @@ export class RemoteContainerExecutionEnv {
207
352
  }
208
353
  async destroy() {
209
354
  if (this.destroyed)
210
- return;
355
+ return; // idempotent — safe double-call (Runner end + reaper)
211
356
  this.destroyed = true;
212
357
  this.connecting = undefined;
213
- this.stopKeepAlive();
214
- await this.disposeBackgroundShells();
358
+ this.stopKeepAlive(); // BL-31 — stop extending the sandbox lifetime; it's being killed below.
359
+ await this.disposeBackgroundShells(); // design/103 §3.7 backstop — clears BG timers (the VM kill below reaps the jobs)
215
360
  const sbx = this.sbx;
216
361
  this.sbx = undefined;
217
362
  if (!sbx)
218
- return;
363
+ return; // never connected (lazy task that didn't touch the hand) → nothing to kill, zero bill
219
364
  const startedAt = Date.now();
220
365
  try {
221
366
  await this.rpc("sandbox.kill", () => sbx.kill());
222
367
  this.cfg.metrics?.inc("remote_env_destroy_total", { provider: PROVIDER, result: "ok" });
368
+ // Symmetric with remote_env_connect: a clean teardown line carrying the same sandboxId
369
+ // gives the cleanup leg a log-only proof — no need to cross-check the provider's running-sandbox API.
223
370
  this.cfg.logger?.info?.("remote_env_destroy", { sandboxId: sbx.sandboxId, ms: Date.now() - startedAt });
224
371
  }
225
372
  catch (e) {
373
+ // best-effort, must never throw (cleanup contract)
226
374
  this.cfg.metrics?.inc("remote_env_destroy_total", { provider: PROVIDER, result: "error" });
227
375
  this.cfg.logger?.warn?.("remote_env_destroy_failed", { sandboxId: sbx.sandboxId, ms: Date.now() - startedAt, error: errMsg(e) });
228
376
  }
229
377
  }
378
+ // FileSystem + Shell both declare cleanup(); both map to destroy() (release all VM resources).
230
379
  async cleanup() {
231
380
  await this.destroy();
232
381
  }
382
+ // ─────────────────────────────── Shell: buffered exec ───────────────────────────────
233
383
  async exec(command, options) {
384
+ // Already cancelled → don't lazily provision a billable VM just to immediately abort it.
234
385
  if (options?.abortSignal?.aborted)
235
386
  return { ok: false, error: new ExecutionError("aborted", "exec aborted before start") };
236
- const conn = await this.ensureConnected();
387
+ const conn = await this.ensureConnected(); // lazy: first use provisions the VM
237
388
  if (!conn.ok)
238
389
  return { ok: false, error: new ExecutionError("shell_unavailable", `remote connect failed: ${conn.error.message}`, conn.error) };
239
390
  const sbx = conn.value;
240
- this.activeCommands++;
391
+ this.activeCommands++; // in-flight: suspendVM refuses while > 0 (council #4)
392
+ // design/116 detach (core 1.207, 1.82): the Bash tool threads a `detachSignal` when the operator can mid-flight
393
+ // ctrl+b. `ExecOpts` doesn't type it yet (core-side seam) — read it structurally. When present, tee all output
394
+ // into a rolling-tail pair shaped EXACTLY like the bg driver's per-shell state (E2bBgStream) — adoption is then
395
+ // a zero-copy handover of {handle, stdout, stderr} into the SAME state the launch path builds.
241
396
  const detachSignal = options?.detachSignal;
242
397
  const tee = detachSignal
243
398
  ? {
@@ -251,13 +406,23 @@ export class RemoteContainerExecutionEnv {
251
406
  s.tail.push(b);
252
407
  s.total += b.length;
253
408
  };
254
- let lastProgress = Date.now();
409
+ // Run in the background and keep the handle so an abort can actually KILL the remote process. A plain
410
+ // `commands.run` only aborts the local request — the VM process keeps running (and billing) until the
411
+ // sandbox times out. The handle still buffers stdout/stderr; wait() returns the full CommandResult.
412
+ let lastProgress = Date.now(); // bumped by stdout/stderr → drives the wait()-phase liveness (waitWithLiveness)
255
413
  let handle;
256
414
  try {
415
+ // 🔴 Bound the command-CREATION RPC with the liveness timer — a provider hang here (before the
416
+ // command even starts) used to wait the whole sandbox lifetime. The wait() phase below is then
417
+ // bounded by waitWithLiveness (idle-bound), so BOTH phases of a hang are covered.
257
418
  handle = await this.rpc("commands.run", () => sbx.commands.run(command, {
258
419
  background: true,
259
420
  cwd: this.resolve(options?.cwd ?? this.cwd),
260
421
  ...(options?.env ? { envs: options.env } : {}),
422
+ // Detach-capable exec: the provider command timeout is fixed at creation, so it must not undercut a
423
+ // LATER adoption's BG wall — lift it to the BG ceiling (+backstop pad, same discipline as the bg
424
+ // launch) and enforce the FOREGROUND wall with the control-plane `wallMs` in waitWithLiveness below
425
+ // (disarmed on adoption). Without a detachSignal the provider timeout stays the wall, as before.
261
426
  timeoutMs: tee
262
427
  ? Math.max(this.commandTimeoutMs(options?.timeout), (E2B_BG_MAX_TIMEOUT_SEC + E2B_BG_PROVIDER_BACKSTOP_PAD_SEC) * 1000)
263
428
  : this.commandTimeoutMs(options?.timeout),
@@ -283,6 +448,8 @@ export class RemoteContainerExecutionEnv {
283
448
  this.activeCommands--;
284
449
  return { ok: false, error: this.execError(e, options?.abortSignal) };
285
450
  }
451
+ // Post-adoption the abort belongs to the (already-settled) foreground tool call — it must NOT kill the
452
+ // adopted background job (core aborts the per-call signal right after the early tool result).
286
453
  const onAbort = () => { if (!detached)
287
454
  void handle.kill().catch(() => { }); };
288
455
  if (options?.abortSignal) {
@@ -291,8 +458,10 @@ export class RemoteContainerExecutionEnv {
291
458
  else
292
459
  options.abortSignal.addEventListener("abort", onAbort, { once: true });
293
460
  }
461
+ // 🔴 Consume `handle.wait()` exactly ONCE (the SDK processes the command's event stream in it): the foreground
462
+ // race and the post-adoption terminal notification both hang off this single promise.
294
463
  const waitP = handle.wait();
295
- let fgResolved = false;
464
+ let fgResolved = false; // microtask-latch: a detach landing after the command finished must NOT adopt (double-represent)
296
465
  const markResolved = () => { fgResolved = true; };
297
466
  waitP.then(markResolved, markResolved);
298
467
  let fgSettled = false;
@@ -301,8 +470,19 @@ export class RemoteContainerExecutionEnv {
301
470
  const detachedP = detachSignal && tee
302
471
  ? new Promise((res) => {
303
472
  onDetach = () => {
473
+ // 🔴 review MED: E2B's onAbort only fires an async `handle.kill()` (no synchronous settle flag), unlike
474
+ // host/k8s whose abort synchronously settles (finish→settled) and whose onDetach guards on it. Without
475
+ // `abortSignal?.aborted` here, an abort in flight (kill RPC not yet completed, so `fgResolved` unset)
476
+ // followed by a detach would ADOPT a process that is already being killed — the aborted call would return
477
+ // detached success and hand the operator a corpse shellId. Align E2B with the settled-blocks-detach rule.
304
478
  if (detached || fgSettled || fgResolved || options?.abortSignal?.aborted)
305
479
  return;
480
+ // SYNC adopt (manager contract): the tee buffers already hold every byte from process start (the
481
+ // onStdout/onStderr closures keep feeding them — no listener re-registration exists on an E2B
482
+ // CommandHandle, which is exactly why the tee is created up-front). Terminal rides the shared waitP.
483
+ // 🔴 builder contract (Fable-5 review #5): everything inside is NON-THROWING (.then/.catch on an
484
+ // existing promise + object construction). adoptSync's catch does NOT undo side effects — future
485
+ // logic that can throw must go before the first side effect.
306
486
  const adopted = this.bgManager.adoptSync((ctx) => {
307
487
  waitP
308
488
  .then((r) => ctx.onTerminal(false, r.exitCode))
@@ -315,11 +495,13 @@ export class RemoteContainerExecutionEnv {
315
495
  return { handle, stdout: tee.stdout, stderr: tee.stderr };
316
496
  });
317
497
  if (!adopted)
318
- return;
498
+ return; // refused (limit) → the exec keeps running in the foreground
319
499
  adoptedShellId = adopted.shellId;
320
- detached = true;
500
+ detached = true; // stops caller-forwarding + disarms the fg liveness/wall kills
321
501
  res("detached");
322
502
  };
503
+ // core's ToolDetachHub is create-then-abort: a detach that landed BEFORE this exec started arrives as an
504
+ // ALREADY-aborted signal → adopt immediately (everything onDetach touches is initialized above).
323
505
  if (detachSignal.aborted)
324
506
  onDetach();
325
507
  else
@@ -330,6 +512,7 @@ export class RemoteContainerExecutionEnv {
330
512
  const fg = this.waitWithLiveness(handle, () => lastProgress, {
331
513
  waitP,
332
514
  disarmed: () => detached,
515
+ // Foreground wall, control-plane enforced on the detach-capable path (provider timeout was lifted above).
333
516
  ...(tee ? { wallMs: this.commandTimeoutMs(options?.timeout) } : {}),
334
517
  });
335
518
  const winner = await (detachedP ? Promise.race([fg, detachedP]) : fg);
@@ -343,6 +526,11 @@ export class RemoteContainerExecutionEnv {
343
526
  detached: { shellId: adoptedShellId },
344
527
  });
345
528
  }
529
+ // 夜测 N2(2026-07-09,MED):an aborted exec's Result shape was NON-DETERMINISTIC — the abort path only
530
+ // fires an async handle.kill(), after which wait() may settle as a plain result OR a CommandExitError
531
+ // (non-zero exit from the kill), both previously returned ok:true; only an rpc-layer failure surfaced
532
+ // "aborted". Normalize: an aborted, non-detached exec ALWAYS returns {ok:false, error:"aborted"} (the
533
+ // remote process IS terminated either way — this only stabilizes the caller's Result contract).
346
534
  if (options?.abortSignal?.aborted && !detached) {
347
535
  return { ok: false, error: new ExecutionError("aborted", "exec aborted (remote process killed)") };
348
536
  }
@@ -350,8 +538,9 @@ export class RemoteContainerExecutionEnv {
350
538
  }
351
539
  catch (e) {
352
540
  if (options?.abortSignal?.aborted && !detached) {
353
- return { ok: false, error: this.execError(e, options.abortSignal) };
541
+ return { ok: false, error: this.execError(e, options.abortSignal) }; // N2: classified "aborted" (see above)
354
542
  }
543
+ // E2B throws CommandExitError on non-zero exit, but ExecutionEnv treats exitCode as a normal value.
355
544
  if (e instanceof CommandExitError) {
356
545
  return ok({ stdout: e.stdout, stderr: e.stderr, exitCode: e.exitCode });
357
546
  }
@@ -365,11 +554,13 @@ export class RemoteContainerExecutionEnv {
365
554
  options?.abortSignal?.removeEventListener("abort", onAbort);
366
555
  }
367
556
  }
557
+ // ─────────────────────────────── RemoteExecutionEnv: streaming exec ───────────────────────────────
368
558
  async *execStream(command, options) {
369
- const conn = await this.ensureConnected();
559
+ const conn = await this.ensureConnected(); // lazy: first use provisions the VM
370
560
  if (!conn.ok)
371
561
  throw conn.error;
372
562
  const sbx = conn.value;
563
+ // Producer/consumer queue: E2B delivers output via onStdout/onStderr callbacks; we pull as an iterator.
373
564
  const queue = [];
374
565
  let finished = false;
375
566
  let failure;
@@ -384,11 +575,15 @@ export class RemoteContainerExecutionEnv {
384
575
  const cap = options?.maxOutputBytes;
385
576
  const push = (type, data) => {
386
577
  if (finished)
387
- return;
578
+ return; // terminal-latch: once finished, drop any late E2B callback (never enqueue into a dead stream)
388
579
  bytes += Buffer.byteLength(data, "utf8");
389
580
  queue.push({ type, data });
390
581
  signalReady();
391
582
  };
583
+ // Increment BEFORE awaiting commands.run (mirrors exec): `commands.run` is a round-trip that may start the
584
+ // command on the VM before it resolves, so suspendVM must already see activeCommands > 0 to refuse — else a
585
+ // suspend racing the launch could snapshot a running command (council #4 invariant). Decrement on launch
586
+ // failure here; the normal path decrements in the outer finally.
392
587
  this.activeCommands++;
393
588
  let handle;
394
589
  try {
@@ -402,9 +597,14 @@ export class RemoteContainerExecutionEnv {
402
597
  }), options?.signal, this.cfg.livenessMs);
403
598
  }
404
599
  catch (e) {
600
+ // failed before any exit chunk → throw (streaming analogue of never-throw Result)
405
601
  this.activeCommands--;
406
602
  throw this.remoteError(e, "connect_failed");
407
603
  }
604
+ // Completion: E2B throws CommandExitError on non-zero exit — that is a NORMAL finish, not a stream failure.
605
+ // `finished` is a first-writer-wins latch: whichever of {wait-resolve, wait-reject, abort} fires first sets
606
+ // the terminal state; the rest no-op. Without it, an abort (or a late wait settlement) firing AFTER a clean
607
+ // completion — but before the consumer pulls the final chunk — would clobber a successful exit into a throw.
408
608
  handle
409
609
  .wait()
410
610
  .then((r) => {
@@ -440,13 +640,18 @@ export class RemoteContainerExecutionEnv {
440
640
  else
441
641
  options.signal.addEventListener("abort", onAbort, { once: true });
442
642
  }
643
+ // Drain via a read-index, not Array.shift() — shift() is O(n) per call → O(n²) on large output (a
644
+ // `npm ci`/build streaming millions of lines). Compacts once fully caught up. Throws (after killing the
645
+ // command) on maxOutputBytes. Single helper so the main loop and the post-completion flush stay in lockstep.
443
646
  let head = 0;
444
647
  const drainQueued = function* () {
445
648
  while (head < queue.length) {
446
649
  const chunk = queue[head++];
447
650
  yield chunk;
651
+ // Soft cap: checked AFTER yield, so the chunk that crosses `cap` is still delivered (bounded by one
652
+ // E2B chunk). It's flood-protection (stop a `find /` drowning the control plane), not a byte-exact limit.
448
653
  if (cap != null && bytes > cap) {
449
- finished = true;
654
+ finished = true; // latch before kill (symmetry with onAbort): blocks late callbacks + the finally's re-kill
450
655
  void handle.kill().catch(() => { });
451
656
  throw new RemoteExecutionError("unknown", `execStream output exceeded maxOutputBytes=${cap}`);
452
657
  }
@@ -459,9 +664,15 @@ export class RemoteContainerExecutionEnv {
459
664
  yield* drainQueued();
460
665
  if (finished)
461
666
  break;
667
+ // Idle read-timeout (design/48 §5, E2B #1128): a streaming call has no read timeout, so an
668
+ // unreachable sandbox would hang forever. Bound the wait for the NEXT chunk and kill on expiry.
462
669
  const next = new Promise((resolve) => {
463
670
  wake = resolve;
464
671
  });
672
+ // 🔴 Liveness is DEFAULT-ON: a missing readTimeoutMs MUST NOT mean "wait forever / until the env
673
+ // dies" — fall back to cfg.livenessMs. (The command-CREATION RPC is bounded separately by rpc() above, so
674
+ // the two together cover both phases of a provider hang.) Only kills a ZERO-PROGRESS command — a slow but
675
+ // streaming `npm ci` keeps resetting the timer via push()→signalReady().
465
676
  const idleMs = options?.readTimeoutMs ?? this.cfg.livenessMs;
466
677
  let timer;
467
678
  const idle = new Promise((_, reject) => {
@@ -471,7 +682,7 @@ export class RemoteContainerExecutionEnv {
471
682
  await Promise.race([next, idle]);
472
683
  }
473
684
  catch (e) {
474
- finished = true;
685
+ finished = true; // latch before kill (symmetry with onAbort): blocks late callbacks + the finally's re-kill
475
686
  void handle.kill().catch(() => { });
476
687
  this.cfg.metrics?.inc("remote_env_exec_stream_read_timeout_total", { provider: PROVIDER });
477
688
  throw e instanceof RemoteExecutionError ? e : this.remoteError(e);
@@ -481,12 +692,16 @@ export class RemoteContainerExecutionEnv {
481
692
  clearTimeout(timer);
482
693
  }
483
694
  }
484
- yield* drainQueued();
695
+ yield* drainQueued(); // flush any chunks delivered alongside completion (same cap/kill semantics)
485
696
  if (failure)
486
697
  throw failure;
487
698
  yield { type: "exit", exitCode };
488
699
  }
489
700
  finally {
701
+ // If the consumer abandons the iterator early (`break`/upstream cancel without an abort signal), the
702
+ // generator runs only this finally — the remote command would otherwise keep running (and billing) for
703
+ // its whole timeout, AND activeCommands would drop to 0 with a live command, letting suspendVM snapshot a
704
+ // running command. Kill on any non-finished exit so the council #4 invariant holds on every path.
490
705
  if (!finished) {
491
706
  finished = true;
492
707
  void handle.kill().catch(() => { });
@@ -495,11 +710,34 @@ export class RemoteContainerExecutionEnv {
495
710
  options?.signal?.removeEventListener("abort", onAbort);
496
711
  }
497
712
  }
713
+ // ─────────────────────────── design/103 background shell (TOB, path ②) ───────────────────────────
714
+ //
715
+ // E2B takes path ② (design/103 §5.2): a resident control-plane reader. `commands.run({background:true})` returns
716
+ // a `CommandHandle` immediately whose `onStdout`/`onStderr` callbacks keep firing as output arrives; we tee them
717
+ // into a per-shell rolling-tail buffer and serve `pollBackground` from that buffer's cursor. Consequence (design
718
+ // §5.2): the buffer is REPLICA-AFFINE — it lives in this env instance's heap and does NOT survive a reconnect to
719
+ // another replica. That is fine: background shells never cross a durable suspend (the Runner disposes them before
720
+ // `suspendVM`, design/103 §3.7), and a reconnected replica's empty registry yields `not_found` (VM destroy reaps
721
+ // the orphaned in-sandbox job). Background jobs deliberately do NOT touch `activeCommands` — they are long-lived
722
+ // and would otherwise wedge `suspendVM` forever; the dispose-before-suspend ordering (core saga) is what keeps a
723
+ // running background job from being snapshotted.
724
+ //
725
+ // 🔴 KILL SCOPE (honest, live-verified limitation): `handle.kill()` reliably SIGKILLs the MAIN process the model
726
+ // started — which is the model's KillShell intent (the dev server stops, its port frees). It does NOT group-kill
727
+ // the subtree, and E2B offers no SAFE way to: the per-command process group is SHARED with envd (killing it kills
728
+ // the sandbox), a ppid tree-walk doesn't reach E2B's reparented/disowned descendants, and `/proc/<pid>/environ`
729
+ // is ptrace-hardened (an env-marker scan reads nothing). All probed live. So detached grandchildren are reaped by
730
+ // the sandbox VM teardown (`destroy()`/`sbx.kill()`), which ALWAYS runs at task end — the reliable reaper (mirrors
731
+ // TOC's crash-honesty: we don't fake a group-kill the provider can't safely do). Practical impact is bounded:
732
+ // KillShell stops the server; on dispose-before-suspend the main processes die and any survivor is frozen (paused
733
+ // VM = no CPU) then reaped at the eventual destroy. Relayed to core for the design ledger.
498
734
  backgroundCapabilities = {
499
735
  supported: true,
500
736
  maxConcurrent: E2B_BG_MAX_CONCURRENT,
501
737
  defaultBgTimeoutSec: E2B_BG_DEFAULT_TIMEOUT_SEC,
502
738
  maxBgTimeoutSec: E2B_BG_MAX_TIMEOUT_SEC,
739
+ // core 1.207 design/116 detach (1.82): a running foreground exec can be ADOPTED as a background shell
740
+ // (mid-flight ctrl+b) — the exec's CommandHandle + tee buffers hand over into the launch-shaped state.
503
741
  supportsDetach: true,
504
742
  };
505
743
  _bgManager;
@@ -510,16 +748,21 @@ export class RemoteContainerExecutionEnv {
510
748
  const self = this;
511
749
  return {
512
750
  async launch(command, options, ctx) {
513
- const conn = await self.ensureConnected();
751
+ const conn = await self.ensureConnected(); // lazy: first use provisions the VM
514
752
  if (!conn.ok)
515
- throw new Error(`remote connect failed: ${conn.error.message}`);
753
+ throw new Error(`remote connect failed: ${conn.error.message}`); // → manager wraps as spawn_failed
516
754
  const sbx = conn.value;
517
755
  const stdout = { tail: new RollingTailBuffer(), total: 0, cursor: 0 };
518
756
  const stderr = { tail: new RollingTailBuffer(), total: 0, cursor: 0 };
757
+ // 🔴 secret-scrub red line (design/103 §3.1): env is passed EXACTLY as `exec`/`execStream` do
758
+ // (`{ envs: options.env }`) — the host process.env is never inherited; the sandbox base env is the
759
+ // operator-trusted `sandboxEnv` set at create. A background child therefore sees the SAME scrubbed env.
519
760
  const handle = await self.rpc("commands.run.bg", () => sbx.commands.run(command, {
520
761
  background: true,
521
762
  cwd: self.resolve(options?.cwd ?? self.cwd),
522
763
  ...(options?.env ? { envs: options.env } : {}),
764
+ // Provider-level backstop ONLY: padded past the control-plane hard wall so the manager's timer flips
765
+ // status to `killed` first; this reaps the in-sandbox job if the control plane dies (design/103 §3.6).
523
766
  timeoutMs: (ctx.bgTimeoutSec + E2B_BG_PROVIDER_BACKSTOP_PAD_SEC) * 1000,
524
767
  onStdout: (d) => {
525
768
  const b = Buffer.from(d, "utf8");
@@ -532,6 +775,8 @@ export class RemoteContainerExecutionEnv {
532
775
  stderr.total += b.length;
533
776
  },
534
777
  }), undefined, self.cfg.livenessMs);
778
+ // Push-model terminal (design/103): wait() resolves on clean exit, rejects with CommandExitError on non-zero
779
+ // (a NORMAL finish → exited), or any other error → failed. `.catch` is mandatory (no unhandled rejection).
535
780
  handle
536
781
  .wait()
537
782
  .then((r) => ctx.onTerminal(false, r.exitCode))
@@ -545,10 +790,18 @@ export class RemoteContainerExecutionEnv {
545
790
  },
546
791
  async read(state) {
547
792
  const slice = (s) => {
793
+ // Cursor over ABSOLUTE byte offsets; the tail retains [droppedBytes, total). Return [cursor, total).
548
794
  const { text, droppedBytes } = s.tail.result();
549
795
  const droppedBeforeCursor = Math.max(0, droppedBytes - s.cursor);
550
796
  const startByte = Math.max(s.cursor, droppedBytes);
551
797
  const startInTail = startByte - droppedBytes;
798
+ // 🔴 re-encode delta (Fable-5 review #2): `startInTail` is an ORIGINAL-byte offset, but `text` is a
799
+ // decode→re-encode round trip. The tail's content is always valid UTF-8 (fed from SDK strings) EXCEPT
800
+ // its very head when an evict partially cut a multibyte codepoint — each stranded byte decodes to one
801
+ // U+FFFD (3 bytes re-encoded), so the re-encoded buffer is `delta` bytes longer than the original tail
802
+ // and the difference sits entirely BEFORE any valid cursor position (the cursor always lands on an SDK
803
+ // string boundary, past the stranded head). Shift the slice start by delta; without it the increment
804
+ // re-served up to 2 already-returned bytes per stranded byte (byte-drifted output).
552
805
  const reencoded = Buffer.from(text, "utf8");
553
806
  const delta = reencoded.length - (s.total - droppedBytes);
554
807
  const inc = startInTail <= 0 ? text : reencoded.subarray(startInTail + delta).toString("utf8");
@@ -565,10 +818,10 @@ export class RemoteContainerExecutionEnv {
565
818
  };
566
819
  },
567
820
  async kill(state) {
568
- await state.handle.kill().catch(() => { });
821
+ await state.handle.kill().catch(() => { }); // idempotent: no-op if already settled
569
822
  },
570
823
  async disposeOne(state) {
571
- await state.handle.kill().catch(() => { });
824
+ await state.handle.kill().catch(() => { }); // best-effort; the VM destroy reaps anything that survives
572
825
  },
573
826
  };
574
827
  }
@@ -582,8 +835,11 @@ export class RemoteContainerExecutionEnv {
582
835
  return this.bgManager.kill(shellId);
583
836
  }
584
837
  disposeBackgroundShells(opts) {
838
+ // 签名统一带 opts(core 尾调 {except: keepAlive})。隔离 env 的留活是名义的——沙箱拆机进程必然随之
839
+ // 消亡(destroy 整体带走),core 的 envDying settle 会先给准确 killed 回执;透传保证签名不静默丢参。
585
840
  return this._bgManager ? this._bgManager.dispose(opts) : Promise.resolve();
586
841
  }
842
+ // ─────────────────────────────── FileSystem ───────────────────────────────
587
843
  async absolutePath(p) {
588
844
  return ok(this.resolve(p));
589
845
  }
@@ -608,7 +864,7 @@ export class RemoteContainerExecutionEnv {
608
864
  return r;
609
865
  let lines = r.value.split(/\r?\n/);
610
866
  if (lines.length > 0 && lines[lines.length - 1] === "")
611
- lines.pop();
867
+ lines.pop(); // drop trailing newline's empty tail
612
868
  if (options?.maxLines != null)
613
869
  lines = lines.slice(0, options.maxLines);
614
870
  return ok(lines);
@@ -630,6 +886,7 @@ export class RemoteContainerExecutionEnv {
630
886
  if (!sbx.ok)
631
887
  return sbx;
632
888
  try {
889
+ // E2B write creates parent directories automatically. Uint8Array → its backing ArrayBuffer slice.
633
890
  const data = typeof content === "string" ? content : toArrayBuffer(content);
634
891
  await this.rpc("files.write", (signal) => sbx.value.files.write(this.resolve(p), data, { signal }), abortSignal, this.cfg.dataTimeoutMs);
635
892
  return ok(undefined);
@@ -639,6 +896,7 @@ export class RemoteContainerExecutionEnv {
639
896
  }
640
897
  }
641
898
  async appendFile(p, content, abortSignal) {
899
+ // E2B has no native append; read-modify-write (v1 single-worker → no concurrent-append race).
642
900
  const abs = this.resolve(p);
643
901
  const existing = await this.readBinaryFile(abs, abortSignal);
644
902
  let base;
@@ -681,6 +939,9 @@ export class RemoteContainerExecutionEnv {
681
939
  return { ok: false, error: this.fileError(e, this.resolve(p), abortSignal) };
682
940
  }
683
941
  }
942
+ /** 见其余 shell 腿上同名方法的旁注(2026-07-25 六腿补齐):core 的敏感路径门在 `canonicalPath` 失败时退到它;
943
+ * 缺它 ⇒ 合法的悬空链接让整个工具调用失败。必须返回**原始链接目标**(`readlink` 不带 `-f`)。
944
+ * 本腿的文件面也走 `exec`(与 canonicalPath 同通道),所以实现方式与 shell 腿一致。 */
684
945
  async readLink(p, abortSignal) {
685
946
  const abs = this.resolve(p);
686
947
  const r = await this.exec(`readlink -- ${shellQuote(abs)}`, { abortSignal });
@@ -696,6 +957,7 @@ export class RemoteContainerExecutionEnv {
696
957
  if (!r.ok)
697
958
  return { ok: false, error: fileErrorFromExec(r.error, abs) };
698
959
  if (r.value.exitCode !== 0) {
960
+ // realpath can fail for more than missing paths — classify like fileError() instead of collapsing to not_found.
699
961
  const msg = r.value.stderr.trim();
700
962
  if (/permission denied|access denied/i.test(msg))
701
963
  return { ok: false, error: new FileError("permission_denied", msg, abs) };
@@ -716,7 +978,7 @@ export class RemoteContainerExecutionEnv {
716
978
  catch (e) {
717
979
  const fe = this.fileError(e, this.resolve(p), abortSignal);
718
980
  if (fe.code === "not_found")
719
- return ok(false);
981
+ return ok(false); // missing path is `false`, not an error (contract)
720
982
  return { ok: false, error: fe };
721
983
  }
722
984
  }
@@ -725,6 +987,7 @@ export class RemoteContainerExecutionEnv {
725
987
  if (!sbx.ok)
726
988
  return sbx;
727
989
  try {
990
+ // E2B makeDir creates necessary parent directories; returns false if it already exists (treated as ok).
728
991
  await this.rpc("files.makeDir", (signal) => sbx.value.files.makeDir(this.resolve(p), { signal }), options?.abortSignal);
729
992
  return ok(undefined);
730
993
  }
@@ -738,6 +1001,7 @@ export class RemoteContainerExecutionEnv {
738
1001
  return sbx;
739
1002
  const abs = this.resolve(p);
740
1003
  if (options?.recursive) {
1004
+ // E2B files.remove does not guarantee recursive directory removal → shell rm -rf.
741
1005
  const flags = options.force ? "-rf" : "-r";
742
1006
  const r = await this.exec(`rm ${flags} -- ${shellQuote(abs)}`, { abortSignal: options.abortSignal });
743
1007
  if (!r.ok)
@@ -768,9 +1032,17 @@ export class RemoteContainerExecutionEnv {
768
1032
  const r = await this.exec(`mktemp ${suffixArg} ${shellQuote(`${prefix}XXXXXX`)}`, { abortSignal: options?.abortSignal });
769
1033
  return this.tempResult(r, "mktemp");
770
1034
  }
1035
+ // ─────────────────────────────── internals ───────────────────────────────
771
1036
  resolve(p) {
772
1037
  return path.posix.isAbsolute(p) ? path.posix.normalize(p) : path.posix.normalize(path.posix.join(this.cwd, p));
773
1038
  }
1039
+ /**
1040
+ * 🔴 (hang class-fix) Bound a CONTROL-PLANE provider RPC (create/resume/pause/kill/isRunning/files.*) with a
1041
+ * client-side wall-clock deadline + the caller's AbortSignal. Without this, an E2B RPC that hangs (the original
1042
+ * bug: ESTABLISHED socket, no response) pins the worker for the whole sandbox lifetime. On deadline we abort the
1043
+ * op and throw a typed, retryable {@link RemoteExecutionError} code `"timeout"` (caller retries per idempotency —
1044
+ * the adapter NEVER blind-retries). A caller AbortSignal still aborts independently (op surfaces it / "aborted").
1045
+ */
774
1046
  async rpc(label, op, userSignal, timeoutMs = this.cfg.rpcTimeoutMs) {
775
1047
  const deadline = new AbortController();
776
1048
  const timer = setTimeout(() => deadline.abort(), timeoutMs);
@@ -781,24 +1053,55 @@ export class RemoteContainerExecutionEnv {
781
1053
  reject(new RemoteExecutionError("timeout", `remote ${label} exceeded ${timeoutMs}ms (provider RPC hang)`));
782
1054
  }, { once: true }));
783
1055
  try {
1056
+ // RACE the op against the deadline: even if the provider SDK ignores `signal` (e.g. create/isRunning/kill),
1057
+ // the worker is freed on timeout (the op may leak in the background until the SDK's own timeout — but it no
1058
+ // longer PINS us for the sandbox lifetime, the original hang bug). Passing `signal` lets SDK methods that DO
1059
+ // honor it (files.*, pause, connect) actually cancel the request. A caller `userSignal` abort propagates via
1060
+ // `signal` → the op rejects with its own abort error (NOT surfaced as "timeout"; deadline never fired).
784
1061
  return await Promise.race([op(signal), onDeadline]);
785
1062
  }
786
1063
  finally {
787
1064
  clearTimeout(timer);
788
- combined?.dispose();
1065
+ combined?.dispose(); // council BUG-2: release the fallback anySignal listeners (no-op for AbortSignal.any)
789
1066
  }
790
1067
  }
1068
+ /**
1069
+ * Per-command wall-clock for the E2B SDK (seconds→ms). 🔴 This is a PHYSICAL CAP, NOT the hang
1070
+ * detector. The old code returned `this.cfg.timeoutMs` (the sandbox lifetime) when no `timeout` was given AND
1071
+ * had no liveness check → a hung command waited the WHOLE lifetime (~30 min, the original hang bug). The fix is
1072
+ * NOT to shorten this cap (that would spuriously kill a long-but-progressing `npm ci`) — it's to add a LIVENESS
1073
+ * bound ({@link cfg.livenessMs}) in exec/execStream that catches a *zero-progress* command in ~livenessMs,
1074
+ * independent of this cap. So: explicit caller `timeout` wins; else the lifetime is the cap (a command can't
1075
+ * outlive its VM) but hang detection no longer depends on it.
1076
+ */
791
1077
  commandTimeoutMs(timeoutSec) {
792
1078
  return timeoutSec != null ? timeoutSec * 1000 : this.cfg.timeoutMs;
793
1079
  }
794
- async waitWithLiveness(handle, lastProgress, opts) {
1080
+ /**
1081
+ * 🔴 Liveness for the buffered exec WAIT phase. `handle.wait()` is a long-poll with no read timeout —
1082
+ * a command that hangs DURING execution (not creation) would otherwise wait the E2B command timeout (= the
1083
+ * lifetime when none is set, the original hang bug). Races wait() against an idle check: if no stdout/stderr for
1084
+ * `livenessMs`, the command is hung → kill + typed retryable "timeout". `lastProgress()` is bumped by the
1085
+ * stdout/stderr callbacks, so a slow-but-PROGRESSING command (long `npm ci`) is never killed. (execStream has
1086
+ * its own chunk-driven idle timer; this is the buffered-exec analogue.)
1087
+ */
1088
+ async waitWithLiveness(handle, lastProgress,
1089
+ // design/116 detach: `waitP` shares the ONE `handle.wait()` consumption with exec's adoption path; `disarmed`
1090
+ // (⇒ the command was adopted as a background shell) makes the liveness/wall timers dead switches — the fg leg
1091
+ // must NEVER kill a process the bgManager now owns; `wallMs` is the control-plane foreground wall for the
1092
+ // detach-capable path (whose provider timeout was lifted to the BG ceiling).
1093
+ opts) {
795
1094
  let timer;
796
1095
  let wallTimer;
1096
+ // council DESIGN-1: a fixed 5s polling setInterval can fire AT the deadline and kill a command that just
1097
+ // finished (race vs handle.wait settling, discarding its exitCode). Instead arm a setTimeout for EXACTLY the
1098
+ // remaining idle window; when it fires, re-check lastProgress() — if output advanced it, re-arm for the new
1099
+ // remainder (so a slow-but-progressing command is never killed), else it is genuinely idle → kill + timeout.
797
1100
  const idle = new Promise((_, reject) => {
798
1101
  const arm = () => {
799
1102
  timer = setTimeout(() => {
800
1103
  if (opts?.disarmed?.())
801
- return;
1104
+ return; // adopted: the BG wall owns the process now — never kill from the fg leg
802
1105
  const idleFor = Date.now() - lastProgress();
803
1106
  if (idleFor >= this.cfg.livenessMs) {
804
1107
  void handle.kill().catch(() => { });
@@ -806,7 +1109,7 @@ export class RemoteContainerExecutionEnv {
806
1109
  reject(new RemoteExecutionError("timeout", `exec idle > ${this.cfg.livenessMs}ms (provider hang / unreachable sandbox)`));
807
1110
  }
808
1111
  else {
809
- arm();
1112
+ arm(); // progress advanced lastProgress since arming → re-arm for the now-remaining window
810
1113
  }
811
1114
  }, Math.max(this.cfg.livenessMs - (Date.now() - lastProgress()), 0));
812
1115
  };
@@ -821,6 +1124,8 @@ export class RemoteContainerExecutionEnv {
821
1124
  }
822
1125
  });
823
1126
  try {
1127
+ // handle.wait() settling (resolve OR CommandExitError reject) wins the race over a still-pending timer, so a
1128
+ // command that completes is never killed; finally clears the timer on every exit path.
824
1129
  return await Promise.race([opts?.waitP ?? handle.wait(), idle]);
825
1130
  }
826
1131
  finally {
@@ -830,7 +1135,10 @@ export class RemoteContainerExecutionEnv {
830
1135
  clearTimeout(wallTimer);
831
1136
  }
832
1137
  }
1138
+ /** Lazily provision the VM on first use: a task that never touches the hand pays zero cost.
1139
+ * Concurrent first-calls share one provision via the memoized {@link connect}. */
833
1140
  async ensureConnected() {
1141
+ // A paused (suspended) env's this.sbx is a dead handle — fail clearly rather than running fs/exec against it.
834
1142
  if (this.paused)
835
1143
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env is suspended (paused) — resume before fs/exec") };
836
1144
  if (this.sbx)
@@ -872,10 +1180,16 @@ export class RemoteContainerExecutionEnv {
872
1180
  const name = e?.name;
873
1181
  if (abortSignal?.aborted || name === "AbortError")
874
1182
  return new FileError("aborted", msg, p);
1183
+ // A remote-fs RPC liveness timeout arrives as RemoteExecutionError("timeout"); surface it on the FileSystem
1184
+ // seam as the symmetric FileError("timeout") (core 1.77.0 added "timeout" to FileErrorCode too — [R71]) so the
1185
+ // retryable-hang signal travels as a typed code, not just a message. + a metric so a file-RPC hang stays
1186
+ // observable. Minor: file RPCs rarely hang and only past dataTimeoutMs (5m).
875
1187
  if (e instanceof RemoteExecutionError && e.code === "timeout") {
876
1188
  this.cfg.metrics?.inc("remote_env_file_timeout_total", { provider: PROVIDER });
877
1189
  return new FileError("timeout", msg, p, e);
878
1190
  }
1191
+ // The sandbox itself vanishing (reaped/timed-out) is an INFRASTRUCTURE failure, not a confirmed-absent
1192
+ // file — never collapse it to not_found, or exists()/stat() would report a dead VM as "file is absent".
879
1193
  if (name === "SandboxNotFoundError" || /sandbox .*not found|sandbox .*not running|sandbox was not found/i.test(msg)) {
880
1194
  return new FileError("unknown", msg, p);
881
1195
  }
@@ -909,15 +1223,30 @@ export class RemoteContainerExecutionEnv {
909
1223
  return new RemoteExecutionError(code, errMsg(e), e instanceof Error ? e : undefined);
910
1224
  }
911
1225
  }
1226
+ /**
1227
+ * Build the deployment-level `RunnerDeps.executionEnvFactory` (design/48 v1b). Wiring this onto a
1228
+ * deployment makes it a "code-agent worker": every task's hand runs in a per-task E2B VM. Deployments that do
1229
+ * NOT set this (e.g. OA / review) keep the in-process stub env — routing is **deployment-level** (the factory
1230
+ * ctx carries only sessionId/taskId, never a service scenario; that stays out of the generic core seam).
1231
+ *
1232
+ * The factory returns the env **unconnected** (lazy): the VM is provisioned on first FS/Shell/execStream use,
1233
+ * so a task that never touches the hand (e.g. a leader that only plans) pays zero VM cost. The Runner owns the
1234
+ * lifetime and calls `destroy()` on task end (via `hasDestroy`); a never-used env's destroy is a no-op.
1235
+ */
912
1236
  export function e2bExecutionEnvFactory(config, deps) {
913
1237
  return (ctx) => new RemoteContainerExecutionEnv({
914
1238
  ...config,
915
1239
  metadata: { ...config.metadata, sessionId: ctx.sessionId, ...(ctx.taskId ? { taskId: ctx.taskId } : {}) },
916
1240
  }, deps);
917
1241
  }
1242
+ // ─────────────────────────────── module helpers ───────────────────────────────
918
1243
  function errMsg(e) {
919
1244
  return e instanceof Error ? e.message : String(e);
920
1245
  }
1246
+ /** E2B FilesystemRequestOpts `{ signal }` spread helper (omit the key entirely when no signal). */
1247
+ /** Combine signals into one that aborts when ANY input does (used by {@link RemoteContainerExecutionEnv.rpc} to
1248
+ * race the caller's abort with the client-side deadline). Uses the platform `AbortSignal.any` when present
1249
+ * (Node ≥20 / Bun), else a small fallback. */
921
1250
  function anySignal(signals) {
922
1251
  const ctor = AbortSignal;
923
1252
  if (typeof ctor.any === "function")
@@ -931,13 +1260,15 @@ function anySignal(signals) {
931
1260
  break;
932
1261
  }
933
1262
  s.addEventListener("abort", onAbort, { once: true });
934
- cleanups.push(() => s.removeEventListener("abort", onAbort));
1263
+ cleanups.push(() => s.removeEventListener("abort", onAbort)); // council BUG-2: dispose() removes these on long-lived signals
935
1264
  }
936
1265
  return { signal: ctrl.signal, dispose: () => cleanups.forEach((c) => c()) };
937
1266
  }
1267
+ /** POSIX single-quote a string for safe shell interpolation. */
938
1268
  function shellQuote(s) {
939
1269
  return `'${s.replace(/'/g, `'\\''`)}'`;
940
1270
  }
1271
+ /** Copy a Uint8Array's exact bytes into a standalone ArrayBuffer (E2B write accepts ArrayBuffer). */
941
1272
  function toArrayBuffer(u) {
942
1273
  return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
943
1274
  }