@sema-agent/server 7.43.0 → 7.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (345) hide show
  1. package/USAGE.md +18 -0
  2. package/dist/adoption/plan.js +31 -192
  3. package/dist/adoption/quiesce.js +3 -86
  4. package/dist/adoption/runner.js +3 -140
  5. package/dist/adoption/sql.js +0 -74
  6. package/dist/adoption/wire.js +0 -80
  7. package/dist/approval-ask-machine.js +0 -75
  8. package/dist/approval-card.js +0 -323
  9. package/dist/approval-deny-reasons.js +0 -42
  10. package/dist/approval-hmac.js +0 -35
  11. package/dist/approval-reconciler.js +8 -166
  12. package/dist/approval.js +4 -67
  13. package/dist/audit.js +1 -44
  14. package/dist/auth-bridge.js +4 -47
  15. package/dist/auth-keys.js +0 -23
  16. package/dist/bake-runner/main.js +4 -65
  17. package/dist/bake-runner/protocol.js +3 -58
  18. package/dist/bake-runner/runner.js +5 -91
  19. package/dist/bench/l8/artifact.js +2 -61
  20. package/dist/bench/l8/escape.js +0 -25
  21. package/dist/bench/l8/index.js +0 -14
  22. package/dist/bench/l8/probes.js +0 -60
  23. package/dist/bench/l8/run-probes.js +2 -45
  24. package/dist/bench/s1/arms.js +14 -189
  25. package/dist/bench/s1/live-deps.js +11 -253
  26. package/dist/bench/s1/oracle.js +1 -36
  27. package/dist/bench/s1/repair-oracle-adapter.js +0 -30
  28. package/dist/bench/s1/reviewer.js +0 -34
  29. package/dist/bench/s1/row.js +0 -62
  30. package/dist/bench/s1/run-firm.js +4 -70
  31. package/dist/bench/s1/runner-ctx.js +0 -40
  32. package/dist/bench/s1/tasks.js +0 -115
  33. package/dist/boot/adoption.js +0 -20
  34. package/dist/boot/budget-tracing.js +3 -55
  35. package/dist/boot/config-center.js +43 -551
  36. package/dist/boot/coordinators.js +1 -69
  37. package/dist/boot/crash-last.js +0 -18
  38. package/dist/boot/deferred-sandbox-path-env.js +3 -119
  39. package/dist/boot/execution-env.js +5 -171
  40. package/dist/boot/governance-seams.js +0 -92
  41. package/dist/boot/leader.js +0 -69
  42. package/dist/boot/limit-sync.js +0 -2
  43. package/dist/boot/memory-boundary.js +3 -91
  44. package/dist/boot/org-memory.js +1 -25
  45. package/dist/boot/parked-revive-gate.js +0 -150
  46. package/dist/boot/permission-rules-audit.js +1 -56
  47. package/dist/boot/reapers.js +15 -273
  48. package/dist/boot/resolve-spec.js +9 -768
  49. package/dist/boot/retention-lane.js +4 -87
  50. package/dist/boot/runner-deps.js +3 -297
  51. package/dist/boot/runtime-caps.js +0 -40
  52. package/dist/boot/session-faces.js +2 -133
  53. package/dist/boot/shutdown.js +9 -83
  54. package/dist/boot/side-query-lane.js +2 -137
  55. package/dist/boot/stores.js +15 -215
  56. package/dist/boot/task-list-lane.js +0 -18
  57. package/dist/boot/webfetch-summarize-lane.js +0 -47
  58. package/dist/boot/workflow-orchestration.js +3 -99
  59. package/dist/boot-reclaim.js +0 -20
  60. package/dist/bounded-session-map.js +0 -19
  61. package/dist/brain.js +2 -139
  62. package/dist/budget.js +9 -244
  63. package/dist/capabilities/builtin-tools.js +0 -3
  64. package/dist/capabilities/center-plugins.js +6 -51
  65. package/dist/capabilities/center-prompts.js +6 -71
  66. package/dist/capabilities/code-review-council.js +7 -40
  67. package/dist/capabilities/collab-workflows.js +1 -44
  68. package/dist/capabilities/hands-lane.js +0 -65
  69. package/dist/capabilities/memory-notice.js +0 -70
  70. package/dist/capabilities/prompt.js +0 -14
  71. package/dist/capabilities/prompts/code-review.js +0 -14
  72. package/dist/capabilities/prompts/identity.js +0 -6
  73. package/dist/capabilities/prompts/team.js +0 -4
  74. package/dist/capabilities/repo-tools.js +0 -34
  75. package/dist/capabilities/sandbox-file-send.js +3 -92
  76. package/dist/capabilities/scenarios.d.ts +0 -1
  77. package/dist/capabilities/scenarios.js +18 -279
  78. package/dist/capabilities/select-environment-tool.js +0 -32
  79. package/dist/capabilities/send-user-file-tool.js +2 -66
  80. package/dist/capabilities/skills.d.ts +1 -2
  81. package/dist/capabilities/skills.js +3 -21
  82. package/dist/capabilities/team.d.ts +3 -11
  83. package/dist/capabilities/team.js +3 -51
  84. package/dist/capabilities/tool-defer.js +0 -3
  85. package/dist/config-center/apply-effective.js +15 -330
  86. package/dist/config-center/apply-ledger.js +5 -38
  87. package/dist/config-center/facade.js +0 -41
  88. package/dist/config-center/hot-keys-registry.js +0 -20
  89. package/dist/config-center/http-client.js +2 -125
  90. package/dist/config-center/mcp-revocation.js +2 -34
  91. package/dist/config-center/read-face.js +0 -59
  92. package/dist/config-center/restart-signal.js +1 -85
  93. package/dist/config-center/skills-mcp.d.ts +1 -1
  94. package/dist/config-center/skills-mcp.js +9 -76
  95. package/dist/config-center/stage-limits.js +8 -35
  96. package/dist/config-invariants.js +0 -16
  97. package/dist/config-lkg.js +0 -42
  98. package/dist/config-provider.js +3 -186
  99. package/dist/config-types.js +0 -5
  100. package/dist/config.js +107 -1145
  101. package/dist/degenerate-instrument.js +3 -67
  102. package/dist/deployment-governance.js +0 -124
  103. package/dist/digest-form.js +0 -12
  104. package/dist/elicitation.js +3 -86
  105. package/dist/env-facts.js +7 -75
  106. package/dist/fleet/fleet-bus.js +34 -507
  107. package/dist/fleet/fleet-reconciler.js +9 -149
  108. package/dist/fleet/fleet-terminal-window.js +10 -178
  109. package/dist/fleet/subagent-tail-bus.js +3 -72
  110. package/dist/fleet-client.js +10 -70
  111. package/dist/fleet-lease.js +5 -79
  112. package/dist/git-api-kind.js +0 -3
  113. package/dist/governance-ask-marks.js +2 -78
  114. package/dist/hooks/branch-transcript.js +0 -74
  115. package/dist/hooks/cc-agent-hook-prompt.js +0 -29
  116. package/dist/hooks/cc-stop-prompt.js +1 -46
  117. package/dist/hooks/hook-llm.js +1 -53
  118. package/dist/hooks/hook-runner.js +20 -414
  119. package/dist/http/active-run-conflict.js +4 -129
  120. package/dist/http/cursor-fingerprint.d.ts +5 -0
  121. package/dist/http/cursor-fingerprint.js +5 -0
  122. package/dist/http/idempotency.js +0 -37
  123. package/dist/http/principal-gate.js +3 -40
  124. package/dist/http/route-ctx.js +0 -9
  125. package/dist/http/routes/a2a-serve.js +7 -319
  126. package/dist/http/routes/admin-config-refresh.js +0 -4
  127. package/dist/http/routes/admin-drain.js +0 -10
  128. package/dist/http/routes/adoption.js +1 -29
  129. package/dist/http/routes/agents-roster.js +1 -47
  130. package/dist/http/routes/approvals-assistant.js +32 -395
  131. package/dist/http/routes/attachments.js +4 -22
  132. package/dist/http/routes/capabilities.js +3 -420
  133. package/dist/http/routes/diagnostics.js +1 -81
  134. package/dist/http/routes/fleet.js +9 -185
  135. package/dist/http/routes/images.js +14 -239
  136. package/dist/http/routes/leader.js +0 -13
  137. package/dist/http/routes/memory-bundle.js +1 -61
  138. package/dist/http/routes/memory-policy.js +9 -99
  139. package/dist/http/routes/notify-wake.js +3 -37
  140. package/dist/http/routes/observability.js +2 -19
  141. package/dist/http/routes/retention-ops.js +2 -34
  142. package/dist/http/routes/rules.js +1 -93
  143. package/dist/http/routes/runs.js +64 -873
  144. package/dist/http/routes/session-sync.js +19 -258
  145. package/dist/http/routes/sessions-list.js +17 -43
  146. package/dist/http/routes/sessions.js +34 -226
  147. package/dist/http/routes/shared-memory.js +5 -36
  148. package/dist/http/routes/side-query.js +1 -87
  149. package/dist/http/routes/tasks.js +52 -722
  150. package/dist/http/routes/trace-usage.js +32 -195
  151. package/dist/http/routes/workflows.js +19 -195
  152. package/dist/http/run-meta.js +0 -6
  153. package/dist/http/send.js +0 -32
  154. package/dist/http/server.js +92 -1597
  155. package/dist/http/sse-lifecycle.js +2 -13
  156. package/dist/http/sse-log.js +3 -48
  157. package/dist/http/tar.js +5 -21
  158. package/dist/http/verify-rounds.js +0 -5
  159. package/dist/http/wire-gate.js +0 -9
  160. package/dist/http/workspace-content.js +0 -10
  161. package/dist/images/bake-validate.js +1 -70
  162. package/dist/images/manifest.js +1 -6
  163. package/dist/index.js +0 -21
  164. package/dist/key-resolver.js +2 -17
  165. package/dist/leader/diffout.js +1 -20
  166. package/dist/leader/diffup.js +0 -47
  167. package/dist/leader/endpoint.js +2 -57
  168. package/dist/leader/fanout.js +3 -45
  169. package/dist/leader/grader-env-factory.js +3 -72
  170. package/dist/leader/leader.js +5 -156
  171. package/dist/leader/merge.js +8 -115
  172. package/dist/leader/planner.js +3 -54
  173. package/dist/leader/repair-oracle.js +1 -60
  174. package/dist/leader/repair-wire.js +2 -79
  175. package/dist/leader/wire.js +8 -307
  176. package/dist/lsp/e2b-bridge.js +4 -64
  177. package/dist/lsp/e2b-manager.js +6 -94
  178. package/dist/lsp/lsp-frames.js +0 -12
  179. package/dist/lsp/manager.js +4 -96
  180. package/dist/lsp/ws-transport.js +5 -55
  181. package/dist/lsp-evict.js +1 -15
  182. package/dist/main.js +57 -765
  183. package/dist/memory-bundle-engine.js +0 -55
  184. package/dist/memory-export.js +0 -4
  185. package/dist/memory-posture.js +1 -15
  186. package/dist/memory-scope.js +11 -148
  187. package/dist/memory-sync-client.js +2 -44
  188. package/dist/memory-sync.js +1 -80
  189. package/dist/model-select.js +3 -80
  190. package/dist/observability/cost-quota.js +1 -17
  191. package/dist/observability/cost-taxonomy.js +0 -34
  192. package/dist/observability/fail-open.js +7 -86
  193. package/dist/observability/logger.js +0 -6
  194. package/dist/observability/metrics.js +0 -94
  195. package/dist/observability/otel-exporter.js +3 -13
  196. package/dist/observability/principal-context.js +0 -9
  197. package/dist/observability/prompt-manifest.js +1 -37
  198. package/dist/observability/rate-limit.js +0 -4
  199. package/dist/observability/secret-env-scrub.js +2 -56
  200. package/dist/observability/tool-trace.js +1 -70
  201. package/dist/orchestration/hardened-vm-runner.js +4 -118
  202. package/dist/orchestration/hardened-vm-worker-runner.js +1 -26
  203. package/dist/orchestration/hardened-vm-worker.js +0 -27
  204. package/dist/orchestration/subagent-steer.js +1 -45
  205. package/dist/orchestration/workflow-agent-steer.js +1 -80
  206. package/dist/orchestration/workflow-completion-inbox.js +32 -285
  207. package/dist/orchestration/workflow-notify-journal.js +16 -259
  208. package/dist/org-memory-admission.js +3 -47
  209. package/dist/parent-watch.js +2 -48
  210. package/dist/parked-decide.js +1 -109
  211. package/dist/per-task-image.js +0 -57
  212. package/dist/plan-cache-probe.js +3 -27
  213. package/dist/plugins/adoption-log-sql.js +2 -119
  214. package/dist/plugins/approval-ask-store-memory.js +3 -38
  215. package/dist/plugins/approval-ask-store-sql.js +6 -188
  216. package/dist/plugins/approval-exemption-store.js +2 -28
  217. package/dist/plugins/background-agent-store-sql.js +4 -105
  218. package/dist/plugins/background-shell-support.js +14 -122
  219. package/dist/plugins/blob-backend.js +6 -169
  220. package/dist/plugins/breaker-state-sql.js +8 -46
  221. package/dist/plugins/caching-session-store.js +4 -106
  222. package/dist/plugins/checkpoint-store-sql.js +18 -505
  223. package/dist/plugins/e2b-orphan-reclaim.js +0 -45
  224. package/dist/plugins/file-outcome-sink.js +0 -9
  225. package/dist/plugins/file-resume-anchor-store.js +4 -43
  226. package/dist/plugins/file-run-store.js +26 -364
  227. package/dist/plugins/file-snapshot-store-sql.js +10 -181
  228. package/dist/plugins/fork-routing-session-store.js +8 -111
  229. package/dist/plugins/host-platform.js +2 -91
  230. package/dist/plugins/image-bake-store-sql.js +7 -250
  231. package/dist/plugins/image-index-sql.js +4 -123
  232. package/dist/plugins/k8s-bg-scripts.js +4 -88
  233. package/dist/plugins/k8s-exec-protocol.js +0 -41
  234. package/dist/plugins/leader-run-store-sql.js +0 -101
  235. package/dist/plugins/local-checkpoint-store.js +8 -128
  236. package/dist/plugins/local-session-store.js +34 -296
  237. package/dist/plugins/local-task-attachment-store.js +2 -16
  238. package/dist/plugins/mailbox-store-sql.js +8 -65
  239. package/dist/plugins/memory-embedder-fingerprint.js +5 -166
  240. package/dist/plugins/memory-embedder.js +4 -70
  241. package/dist/plugins/memory-engine-pg.js +6 -170
  242. package/dist/plugins/memory-engine-tidb.js +7 -155
  243. package/dist/plugins/memory-engine-vector-util.js +0 -10
  244. package/dist/plugins/memory-key-guards.js +0 -34
  245. package/dist/plugins/memory-origin-law.js +0 -187
  246. package/dist/plugins/memory-resume-anchor-store.js +0 -17
  247. package/dist/plugins/memory-run-store.js +12 -89
  248. package/dist/plugins/memory-session-policy-store.js +0 -17
  249. package/dist/plugins/memory-sync-store-pg.js +4 -49
  250. package/dist/plugins/memory-sync-store-tidb.js +3 -35
  251. package/dist/plugins/outcome-ledger-sql.js +3 -97
  252. package/dist/plugins/permission-rule-store-file.js +5 -133
  253. package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
  254. package/dist/plugins/permission-rule-store-sql.js +50 -307
  255. package/dist/plugins/pg-cost-quota.js +0 -7
  256. package/dist/plugins/pg-pool.js +0 -92
  257. package/dist/plugins/pg-rate-limiter.js +2 -13
  258. package/dist/plugins/pg-safe-json.js +4 -40
  259. package/dist/plugins/pg-session-storage.js +25 -189
  260. package/dist/plugins/posix-shell-fs.js +1 -31
  261. package/dist/plugins/remote-env-adb.js +12 -101
  262. package/dist/plugins/remote-env-e2b.js +40 -371
  263. package/dist/plugins/remote-env-file-error.js +0 -37
  264. package/dist/plugins/remote-env-host.js +63 -483
  265. package/dist/plugins/remote-env-k8s.js +32 -326
  266. package/dist/plugins/remote-env-local-docker.js +24 -157
  267. package/dist/plugins/remote-env-ssh.js +20 -128
  268. package/dist/plugins/remote-scratchpad.js +2 -32
  269. package/dist/plugins/remote-shell.js +1 -32
  270. package/dist/plugins/resume-anchor-store-sql.js +0 -11
  271. package/dist/plugins/retention-lane-store-sql.js +0 -108
  272. package/dist/plugins/retention-store-sql.js +4 -383
  273. package/dist/plugins/roster-store-sql.js +0 -55
  274. package/dist/plugins/run-store-sql.js +14 -251
  275. package/dist/plugins/s3-presign.js +2 -49
  276. package/dist/plugins/scheduler-support.js +3 -80
  277. package/dist/plugins/send-file-ledger.js +4 -53
  278. package/dist/plugins/send-user-file.js +4 -94
  279. package/dist/plugins/session-placement.js +1 -89
  280. package/dist/plugins/session-policy-store-sql.js +3 -81
  281. package/dist/plugins/session-store.js +0 -57
  282. package/dist/plugins/shared-memory-store-sql.js +4 -167
  283. package/dist/plugins/sql-driver.js +0 -17
  284. package/dist/plugins/sql-errors.js +0 -7
  285. package/dist/plugins/sql-escape.js +0 -8
  286. package/dist/plugins/sql-row-helpers.js +0 -25
  287. package/dist/plugins/store-backend.js +42 -222
  288. package/dist/plugins/store-contracts.js +2 -46
  289. package/dist/plugins/task-attachment-store.js +3 -49
  290. package/dist/plugins/task-list-store-sql.js +0 -79
  291. package/dist/plugins/tidb-cost-quota.js +1 -4
  292. package/dist/plugins/tidb-pool.js +1 -207
  293. package/dist/plugins/tidb-rate-limiter.js +3 -9
  294. package/dist/plugins/tidb-session-storage.js +4 -70
  295. package/dist/plugins/tidb-session-store.js +19 -341
  296. package/dist/plugins/tool-result-store-sql.js +4 -179
  297. package/dist/plugins/usage-window-store-sql.js +0 -8
  298. package/dist/plugins/web-search.js +10 -132
  299. package/dist/plugins/workflow-journal-store-sql.js +2 -58
  300. package/dist/plugins/workflow-run-store-sql.js +6 -91
  301. package/dist/plugins/worktree-isolation.js +6 -126
  302. package/dist/plugins/write-behind-counter.js +16 -75
  303. package/dist/principal-jwt.js +5 -60
  304. package/dist/project-memory.js +15 -146
  305. package/dist/prompts-domain-validate.js +1 -51
  306. package/dist/question.js +2 -111
  307. package/dist/resource-suspend.js +0 -18
  308. package/dist/router/route-orchestration.js +0 -77
  309. package/dist/rules-consent.d.ts +44 -5
  310. package/dist/rules-consent.js +75 -215
  311. package/dist/run-local.js +14 -381
  312. package/dist/runs.js +29 -617
  313. package/dist/runtime-caps-resolver.js +6 -132
  314. package/dist/runtime-governance.js +1 -232
  315. package/dist/sandbox-pkg-source.js +0 -37
  316. package/dist/sealed-key.js +3 -68
  317. package/dist/security.js +6 -291
  318. package/dist/session-leaf-bus.js +0 -32
  319. package/dist/session-sync-content.js +1 -69
  320. package/dist/session-sync-kernel.js +3 -60
  321. package/dist/session-sync.js +3 -66
  322. package/dist/session-titler.js +7 -41
  323. package/dist/session-watch.js +9 -73
  324. package/dist/shared-memory-scope-authorizer.js +0 -18
  325. package/dist/sighup-idle.js +1 -12
  326. package/dist/spec-fields.js +4 -143
  327. package/dist/store-live-probe.js +3 -49
  328. package/dist/task-a2a.js +1 -126
  329. package/dist/task-cwd.js +1 -103
  330. package/dist/task-mcp.js +1 -89
  331. package/dist/task-settings.js +10 -300
  332. package/dist/task-workflow.js +8 -75
  333. package/dist/tool-approval.d.ts +28 -1
  334. package/dist/tool-approval.js +70 -1430
  335. package/dist/trace/artifacts.js +5 -20
  336. package/dist/trace/engine-notice-wire.js +6 -138
  337. package/dist/trace/ledger-sink.js +7 -96
  338. package/dist/trace/project.js +4 -448
  339. package/dist/trace/redact.js +9 -77
  340. package/dist/turn-activity.js +1 -27
  341. package/dist/usage-analytics.js +4 -34
  342. package/dist/wall-clock-jump-guard.js +1 -68
  343. package/package.json +3 -3
  344. package/dist/capabilities/scenario-alias.d.ts +0 -27
  345. package/dist/capabilities/scenario-alias.js +0 -61
@@ -1,24 +1,3 @@
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
- */
22
1
  import path from "node:path";
23
2
  import { Sandbox, CommandExitError, FileType } from "e2b";
24
3
  import { FileError, ExecutionError, RemoteExecutionError, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
@@ -26,91 +5,32 @@ import { fileErrorFromExec } from "./remote-env-file-error.js";
26
5
  import { numEnvOr, shellQuote } from "./remote-shell.js";
27
6
  import { BackgroundShellManager } from "./background-shell-support.js";
28
7
  const PROVIDER = "e2b";
29
- /** E2B default sandbox user home — the workspace root unless overridden. */
30
8
  const DEFAULT_MOUNT_PATH = "/home/user";
31
- /** Default sandbox lifetime if the caller does not set one (E2B bills for this window). */
32
9
  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
- */
42
10
  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
- */
54
11
  const DEFAULT_RPC_TIMEOUT_MS = 60_000;
55
12
  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
- */
64
13
  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
- */
70
14
  const E2B_BG_MAX_CONCURRENT = numEnvOr("E2B_BG_MAX_CONCURRENT", 8, 1);
71
15
  const E2B_BG_DEFAULT_TIMEOUT_SEC = numEnvOr("E2B_BG_DEFAULT_TIMEOUT_SEC", 300, 1);
72
16
  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. */
74
17
  const E2B_BG_PROVIDER_BACKSTOP_PAD_SEC = 30;
75
18
  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
- */
83
19
  export class RemoteContainerExecutionEnv {
84
- /** Current working directory for relative paths (FileSystem contract; mutable, core may reassign). */
85
20
  cwd;
86
21
  cfg;
87
22
  sandboxApi;
88
23
  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. */
94
24
  keepAlive;
95
25
  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. */
99
26
  destroyed = false;
100
- /** In-flight connect/resume, memoized so concurrent lazy first-use OR resume provisions exactly one VM (no double-bill). */
101
27
  connecting;
102
- /** Count of exec/execStream calls in flight — `suspendVM` refuses while > 0 (council #4: never snapshot a running command). */
103
28
  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. */
108
29
  paused = false;
109
30
  constructor(config, deps = {}) {
110
31
  this.cfg = {
111
32
  ...config,
112
33
  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).
114
34
  timeoutMs: Math.min(config.timeoutMs ?? DEFAULT_TIMEOUT_MS, E2B_SANDBOX_MAX_MS),
115
35
  allowInternetAccess: config.allowInternetAccess ?? true,
116
36
  rpcTimeoutMs: config.rpcTimeoutMs ?? DEFAULT_RPC_TIMEOUT_MS,
@@ -120,30 +40,17 @@ export class RemoteContainerExecutionEnv {
120
40
  this.cwd = this.cfg.mountPath;
121
41
  this.sandboxApi = deps.sandboxApi ?? Sandbox;
122
42
  }
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.) */
125
43
  capabilities = { isolation: true, suspendable: true };
126
- // ─────────────────────────────── RemoteExecutionEnv: lifecycle ───────────────────────────────
127
44
  workspaceHandle() {
128
45
  if (!this.handle) {
129
46
  throw new RemoteExecutionError("connect_failed", "workspaceHandle() called before connect()");
130
47
  }
131
48
  return this.handle;
132
49
  }
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
- */
138
50
  async getHost(port) {
139
51
  const conn = await this.ensureConnected();
140
52
  return conn.ok ? conn.value.getHost(port) : undefined;
141
53
  }
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
- */
147
54
  async startBackground(command) {
148
55
  const conn = await this.ensureConnected();
149
56
  if (conn.ok)
@@ -151,57 +58,46 @@ export class RemoteContainerExecutionEnv {
151
58
  }
152
59
  async connect(config) {
153
60
  if (config?.snapshotId) {
154
- // resume-from-snapshot lands via resumeVM (v1.5); connect() just delegates.
155
61
  return this.resumeVM(config.snapshotId, config.abortSignal ? { abortSignal: config.abortSignal } : undefined);
156
62
  }
157
63
  if (this.destroyed)
158
64
  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.
161
65
  if (this.paused)
162
66
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env is suspended (paused) — resumeVM(snapshotId) before use") };
163
67
  if (this.sbx)
164
- return ok(this.workspaceHandle()); // idempotent: already connected
68
+ return ok(this.workspaceHandle());
165
69
  if (this.connecting)
166
- return this.connecting; // concurrent first-use shares one provision (lazy path)
70
+ return this.connecting;
167
71
  this.connecting = this.doConnect(config);
168
72
  const r = await this.connecting;
169
73
  if (!r.ok)
170
- this.connecting = undefined; // failed connect → allow a later retry
74
+ this.connecting = undefined;
171
75
  return r;
172
76
  }
173
77
  async doConnect(config) {
174
78
  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.
179
79
  const sbx = await this.rpc("sandbox.create", () => this.sandboxApi.create({
180
80
  ...(this.cfg.template ? { template: this.cfg.template } : {}),
181
81
  apiKey: this.cfg.apiKey,
182
82
  timeoutMs: this.cfg.timeoutMs,
183
83
  allowInternetAccess: this.cfg.allowInternetAccess,
184
84
  ...(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.
187
85
  ...(this.cfg.sandboxEnv && Object.keys(this.cfg.sandboxEnv).length > 0 ? { envs: this.cfg.sandboxEnv } : {}),
188
86
  }), config?.abortSignal, this.cfg.dataTimeoutMs);
189
87
  if (this.destroyed) {
190
- await sbx.kill().catch(() => { }); // raced with destroy() → kill the just-provisioned VM (no leak)
88
+ await sbx.kill().catch(() => { });
191
89
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env destroyed during connect") };
192
90
  }
193
91
  this.sbx = sbx;
194
92
  this.paused = false;
195
- this.startKeepAlive(); // BL-31 — keep the sandbox alive past its ≤1h lifetime cap while the env is held.
93
+ this.startKeepAlive();
196
94
  this.cfg.logger?.info?.("remote_env_connect", { sandboxId: sbx.sandboxId });
197
95
  this.handle = {
198
96
  sandboxId: sbx.sandboxId,
199
97
  provider: PROVIDER,
200
98
  mountPath: this.cfg.mountPath,
201
- sessionToken: sbx.sandboxId, // a still-running E2B VM is re-attachable by its id (Sandbox.connect)
99
+ sessionToken: sbx.sandboxId,
202
100
  };
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.
205
101
  if (config?.secrets?.length) {
206
102
  this.cfg.logger?.warn?.("remote-env: connect secrets are not injected in v1 (control-plane resolution deferred)", {
207
103
  count: config.secrets.length,
@@ -215,12 +111,6 @@ export class RemoteContainerExecutionEnv {
215
111
  return { ok: false, error: this.remoteError(e, "connect_failed") };
216
112
  }
217
113
  }
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
- */
224
114
  async suspendVM(options) {
225
115
  const sbx = this.sbx;
226
116
  if (!sbx)
@@ -232,18 +122,12 @@ export class RemoteContainerExecutionEnv {
232
122
  }
233
123
  try {
234
124
  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.
239
125
  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.
242
126
  await sbx.kill().catch((e) => this.cfg.logger?.warn?.("remote-env: suspend-recheck kill failed (snapshot may orphan)", { error: errMsg(e) }));
243
127
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env destroyed during suspend") };
244
128
  }
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
129
+ this.paused = true;
130
+ this.connecting = undefined;
247
131
  this.cfg.metrics?.inc("remote_env_suspend_total", { provider: PROVIDER, result: "ok" });
248
132
  this.cfg.logger?.info?.("remote_env_suspend", { snapshotId: sbx.sandboxId });
249
133
  return ok(sbx.sandboxId);
@@ -253,48 +137,30 @@ export class RemoteContainerExecutionEnv {
253
137
  return { ok: false, error: this.remoteError(e, options?.abortSignal?.aborted ? "aborted" : "unknown") };
254
138
  }
255
139
  }
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
- */
262
140
  async resumeVM(snapshotId, options) {
263
141
  if (this.destroyed)
264
142
  return { ok: false, error: new RemoteExecutionError("connect_failed", "env already destroyed") };
265
143
  if (this.sbx && !this.paused && this.sbx.sandboxId === snapshotId)
266
- return ok(this.workspaceHandle()); // already resumed + live (idempotent); a paused sbx must reconnect
144
+ return ok(this.workspaceHandle());
267
145
  if (this.connecting)
268
- return this.connecting; // concurrent resume/connect shares one provision (council #3)
146
+ return this.connecting;
269
147
  this.connecting = this.doResumeVM(snapshotId, options);
270
148
  const r = await this.connecting;
271
149
  if (!r.ok)
272
- this.connecting = undefined; // failed resume → allow a later retry
150
+ this.connecting = undefined;
273
151
  return r;
274
152
  }
275
153
  async doResumeVM(snapshotId, options) {
276
154
  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.
282
155
  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)
286
156
  if (this.destroyed) {
287
157
  await sbx.kill().catch(() => { });
288
158
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env destroyed during resume") };
289
159
  }
290
160
  this.sbx = sbx;
291
161
  this.paused = false;
292
- this.startKeepAlive(); // BL-31 — re-arm the keep-alive on a resumed sandbox too.
162
+ this.startKeepAlive();
293
163
  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 挂起任务。
298
164
  const restoredRoot = options?.priorHandle?.mountPath ?? this.cfg.mountPath;
299
165
  this.cwd = restoredRoot;
300
166
  this.handle = { sandboxId: sbx.sandboxId, provider: PROVIDER, mountPath: restoredRoot, snapshotId, sessionToken: sbx.sandboxId };
@@ -306,17 +172,9 @@ export class RemoteContainerExecutionEnv {
306
172
  return { ok: false, error: this.remoteError(e, options?.abortSignal?.aborted ? "aborted" : "connect_failed") };
307
173
  }
308
174
  }
309
- /** Cross-replica re-attach to a still-running VM = v2 (design/49 council #15: v1.5 always snapshots→resumeVM). */
310
175
  async reconnect(_sessionToken) {
311
176
  return { ok: false, error: new RemoteExecutionError("unknown", "reconnect is deferred to v2 (v1.5 always snapshots → use resumeVM)") };
312
177
  }
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
- */
320
178
  async postResumeInit() {
321
179
  const sbx = this.sbx;
322
180
  if (!sbx)
@@ -331,15 +189,12 @@ export class RemoteContainerExecutionEnv {
331
189
  return { ok: false, error: this.remoteError(e, "post_resume_failed") };
332
190
  }
333
191
  }
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. */
337
192
  startKeepAlive() {
338
193
  this.stopKeepAlive();
339
194
  const everyMs = Math.max(30_000, Math.floor(this.cfg.timeoutMs * 0.8));
340
195
  this.keepAlive = setInterval(() => {
341
196
  if (this.destroyed || this.paused || !this.sbx)
342
- return; // paused/dead → the resume path re-arms it
197
+ return;
343
198
  void this.sbx.setTimeout(this.cfg.timeoutMs).catch((e) => this.cfg.logger?.warn?.("remote_env_keepalive_failed", { error: errMsg(e) }));
344
199
  }, everyMs);
345
200
  this.keepAlive.unref?.();
@@ -352,47 +207,37 @@ export class RemoteContainerExecutionEnv {
352
207
  }
353
208
  async destroy() {
354
209
  if (this.destroyed)
355
- return; // idempotent — safe double-call (Runner end + reaper)
210
+ return;
356
211
  this.destroyed = true;
357
212
  this.connecting = undefined;
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)
213
+ this.stopKeepAlive();
214
+ await this.disposeBackgroundShells();
360
215
  const sbx = this.sbx;
361
216
  this.sbx = undefined;
362
217
  if (!sbx)
363
- return; // never connected (lazy task that didn't touch the hand) → nothing to kill, zero bill
218
+ return;
364
219
  const startedAt = Date.now();
365
220
  try {
366
221
  await this.rpc("sandbox.kill", () => sbx.kill());
367
222
  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.
370
223
  this.cfg.logger?.info?.("remote_env_destroy", { sandboxId: sbx.sandboxId, ms: Date.now() - startedAt });
371
224
  }
372
225
  catch (e) {
373
- // best-effort, must never throw (cleanup contract)
374
226
  this.cfg.metrics?.inc("remote_env_destroy_total", { provider: PROVIDER, result: "error" });
375
227
  this.cfg.logger?.warn?.("remote_env_destroy_failed", { sandboxId: sbx.sandboxId, ms: Date.now() - startedAt, error: errMsg(e) });
376
228
  }
377
229
  }
378
- // FileSystem + Shell both declare cleanup(); both map to destroy() (release all VM resources).
379
230
  async cleanup() {
380
231
  await this.destroy();
381
232
  }
382
- // ─────────────────────────────── Shell: buffered exec ───────────────────────────────
383
233
  async exec(command, options) {
384
- // Already cancelled → don't lazily provision a billable VM just to immediately abort it.
385
234
  if (options?.abortSignal?.aborted)
386
235
  return { ok: false, error: new ExecutionError("aborted", "exec aborted before start") };
387
- const conn = await this.ensureConnected(); // lazy: first use provisions the VM
236
+ const conn = await this.ensureConnected();
388
237
  if (!conn.ok)
389
238
  return { ok: false, error: new ExecutionError("shell_unavailable", `remote connect failed: ${conn.error.message}`, conn.error) };
390
239
  const sbx = conn.value;
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.
240
+ this.activeCommands++;
396
241
  const detachSignal = options?.detachSignal;
397
242
  const tee = detachSignal
398
243
  ? {
@@ -406,23 +251,13 @@ export class RemoteContainerExecutionEnv {
406
251
  s.tail.push(b);
407
252
  s.total += b.length;
408
253
  };
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)
254
+ let lastProgress = Date.now();
413
255
  let handle;
414
256
  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.
418
257
  handle = await this.rpc("commands.run", () => sbx.commands.run(command, {
419
258
  background: true,
420
259
  cwd: this.resolve(options?.cwd ?? this.cwd),
421
260
  ...(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.
426
261
  timeoutMs: tee
427
262
  ? Math.max(this.commandTimeoutMs(options?.timeout), (E2B_BG_MAX_TIMEOUT_SEC + E2B_BG_PROVIDER_BACKSTOP_PAD_SEC) * 1000)
428
263
  : this.commandTimeoutMs(options?.timeout),
@@ -448,8 +283,6 @@ export class RemoteContainerExecutionEnv {
448
283
  this.activeCommands--;
449
284
  return { ok: false, error: this.execError(e, options?.abortSignal) };
450
285
  }
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).
453
286
  const onAbort = () => { if (!detached)
454
287
  void handle.kill().catch(() => { }); };
455
288
  if (options?.abortSignal) {
@@ -458,10 +291,8 @@ export class RemoteContainerExecutionEnv {
458
291
  else
459
292
  options.abortSignal.addEventListener("abort", onAbort, { once: true });
460
293
  }
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.
463
294
  const waitP = handle.wait();
464
- let fgResolved = false; // microtask-latch: a detach landing after the command finished must NOT adopt (double-represent)
295
+ let fgResolved = false;
465
296
  const markResolved = () => { fgResolved = true; };
466
297
  waitP.then(markResolved, markResolved);
467
298
  let fgSettled = false;
@@ -470,19 +301,8 @@ export class RemoteContainerExecutionEnv {
470
301
  const detachedP = detachSignal && tee
471
302
  ? new Promise((res) => {
472
303
  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.
478
304
  if (detached || fgSettled || fgResolved || options?.abortSignal?.aborted)
479
305
  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.
486
306
  const adopted = this.bgManager.adoptSync((ctx) => {
487
307
  waitP
488
308
  .then((r) => ctx.onTerminal(false, r.exitCode))
@@ -495,13 +315,11 @@ export class RemoteContainerExecutionEnv {
495
315
  return { handle, stdout: tee.stdout, stderr: tee.stderr };
496
316
  });
497
317
  if (!adopted)
498
- return; // refused (limit) → the exec keeps running in the foreground
318
+ return;
499
319
  adoptedShellId = adopted.shellId;
500
- detached = true; // stops caller-forwarding + disarms the fg liveness/wall kills
320
+ detached = true;
501
321
  res("detached");
502
322
  };
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).
505
323
  if (detachSignal.aborted)
506
324
  onDetach();
507
325
  else
@@ -512,7 +330,6 @@ export class RemoteContainerExecutionEnv {
512
330
  const fg = this.waitWithLiveness(handle, () => lastProgress, {
513
331
  waitP,
514
332
  disarmed: () => detached,
515
- // Foreground wall, control-plane enforced on the detach-capable path (provider timeout was lifted above).
516
333
  ...(tee ? { wallMs: this.commandTimeoutMs(options?.timeout) } : {}),
517
334
  });
518
335
  const winner = await (detachedP ? Promise.race([fg, detachedP]) : fg);
@@ -526,11 +343,6 @@ export class RemoteContainerExecutionEnv {
526
343
  detached: { shellId: adoptedShellId },
527
344
  });
528
345
  }
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).
534
346
  if (options?.abortSignal?.aborted && !detached) {
535
347
  return { ok: false, error: new ExecutionError("aborted", "exec aborted (remote process killed)") };
536
348
  }
@@ -538,9 +350,8 @@ export class RemoteContainerExecutionEnv {
538
350
  }
539
351
  catch (e) {
540
352
  if (options?.abortSignal?.aborted && !detached) {
541
- return { ok: false, error: this.execError(e, options.abortSignal) }; // N2: classified "aborted" (see above)
353
+ return { ok: false, error: this.execError(e, options.abortSignal) };
542
354
  }
543
- // E2B throws CommandExitError on non-zero exit, but ExecutionEnv treats exitCode as a normal value.
544
355
  if (e instanceof CommandExitError) {
545
356
  return ok({ stdout: e.stdout, stderr: e.stderr, exitCode: e.exitCode });
546
357
  }
@@ -554,13 +365,11 @@ export class RemoteContainerExecutionEnv {
554
365
  options?.abortSignal?.removeEventListener("abort", onAbort);
555
366
  }
556
367
  }
557
- // ─────────────────────────────── RemoteExecutionEnv: streaming exec ───────────────────────────────
558
368
  async *execStream(command, options) {
559
- const conn = await this.ensureConnected(); // lazy: first use provisions the VM
369
+ const conn = await this.ensureConnected();
560
370
  if (!conn.ok)
561
371
  throw conn.error;
562
372
  const sbx = conn.value;
563
- // Producer/consumer queue: E2B delivers output via onStdout/onStderr callbacks; we pull as an iterator.
564
373
  const queue = [];
565
374
  let finished = false;
566
375
  let failure;
@@ -575,15 +384,11 @@ export class RemoteContainerExecutionEnv {
575
384
  const cap = options?.maxOutputBytes;
576
385
  const push = (type, data) => {
577
386
  if (finished)
578
- return; // terminal-latch: once finished, drop any late E2B callback (never enqueue into a dead stream)
387
+ return;
579
388
  bytes += Buffer.byteLength(data, "utf8");
580
389
  queue.push({ type, data });
581
390
  signalReady();
582
391
  };
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.
587
392
  this.activeCommands++;
588
393
  let handle;
589
394
  try {
@@ -597,14 +402,9 @@ export class RemoteContainerExecutionEnv {
597
402
  }), options?.signal, this.cfg.livenessMs);
598
403
  }
599
404
  catch (e) {
600
- // failed before any exit chunk → throw (streaming analogue of never-throw Result)
601
405
  this.activeCommands--;
602
406
  throw this.remoteError(e, "connect_failed");
603
407
  }
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.
608
408
  handle
609
409
  .wait()
610
410
  .then((r) => {
@@ -640,18 +440,13 @@ export class RemoteContainerExecutionEnv {
640
440
  else
641
441
  options.signal.addEventListener("abort", onAbort, { once: true });
642
442
  }
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.
646
443
  let head = 0;
647
444
  const drainQueued = function* () {
648
445
  while (head < queue.length) {
649
446
  const chunk = queue[head++];
650
447
  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.
653
448
  if (cap != null && bytes > cap) {
654
- finished = true; // latch before kill (symmetry with onAbort): blocks late callbacks + the finally's re-kill
449
+ finished = true;
655
450
  void handle.kill().catch(() => { });
656
451
  throw new RemoteExecutionError("unknown", `execStream output exceeded maxOutputBytes=${cap}`);
657
452
  }
@@ -664,15 +459,9 @@ export class RemoteContainerExecutionEnv {
664
459
  yield* drainQueued();
665
460
  if (finished)
666
461
  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.
669
462
  const next = new Promise((resolve) => {
670
463
  wake = resolve;
671
464
  });
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().
676
465
  const idleMs = options?.readTimeoutMs ?? this.cfg.livenessMs;
677
466
  let timer;
678
467
  const idle = new Promise((_, reject) => {
@@ -682,7 +471,7 @@ export class RemoteContainerExecutionEnv {
682
471
  await Promise.race([next, idle]);
683
472
  }
684
473
  catch (e) {
685
- finished = true; // latch before kill (symmetry with onAbort): blocks late callbacks + the finally's re-kill
474
+ finished = true;
686
475
  void handle.kill().catch(() => { });
687
476
  this.cfg.metrics?.inc("remote_env_exec_stream_read_timeout_total", { provider: PROVIDER });
688
477
  throw e instanceof RemoteExecutionError ? e : this.remoteError(e);
@@ -692,16 +481,12 @@ export class RemoteContainerExecutionEnv {
692
481
  clearTimeout(timer);
693
482
  }
694
483
  }
695
- yield* drainQueued(); // flush any chunks delivered alongside completion (same cap/kill semantics)
484
+ yield* drainQueued();
696
485
  if (failure)
697
486
  throw failure;
698
487
  yield { type: "exit", exitCode };
699
488
  }
700
489
  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.
705
490
  if (!finished) {
706
491
  finished = true;
707
492
  void handle.kill().catch(() => { });
@@ -710,34 +495,11 @@ export class RemoteContainerExecutionEnv {
710
495
  options?.signal?.removeEventListener("abort", onAbort);
711
496
  }
712
497
  }
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.
734
498
  backgroundCapabilities = {
735
499
  supported: true,
736
500
  maxConcurrent: E2B_BG_MAX_CONCURRENT,
737
501
  defaultBgTimeoutSec: E2B_BG_DEFAULT_TIMEOUT_SEC,
738
502
  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.
741
503
  supportsDetach: true,
742
504
  };
743
505
  _bgManager;
@@ -748,21 +510,16 @@ export class RemoteContainerExecutionEnv {
748
510
  const self = this;
749
511
  return {
750
512
  async launch(command, options, ctx) {
751
- const conn = await self.ensureConnected(); // lazy: first use provisions the VM
513
+ const conn = await self.ensureConnected();
752
514
  if (!conn.ok)
753
- throw new Error(`remote connect failed: ${conn.error.message}`); // → manager wraps as spawn_failed
515
+ throw new Error(`remote connect failed: ${conn.error.message}`);
754
516
  const sbx = conn.value;
755
517
  const stdout = { tail: new RollingTailBuffer(), total: 0, cursor: 0 };
756
518
  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.
760
519
  const handle = await self.rpc("commands.run.bg", () => sbx.commands.run(command, {
761
520
  background: true,
762
521
  cwd: self.resolve(options?.cwd ?? self.cwd),
763
522
  ...(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).
766
523
  timeoutMs: (ctx.bgTimeoutSec + E2B_BG_PROVIDER_BACKSTOP_PAD_SEC) * 1000,
767
524
  onStdout: (d) => {
768
525
  const b = Buffer.from(d, "utf8");
@@ -775,8 +532,6 @@ export class RemoteContainerExecutionEnv {
775
532
  stderr.total += b.length;
776
533
  },
777
534
  }), 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).
780
535
  handle
781
536
  .wait()
782
537
  .then((r) => ctx.onTerminal(false, r.exitCode))
@@ -790,18 +545,10 @@ export class RemoteContainerExecutionEnv {
790
545
  },
791
546
  async read(state) {
792
547
  const slice = (s) => {
793
- // Cursor over ABSOLUTE byte offsets; the tail retains [droppedBytes, total). Return [cursor, total).
794
548
  const { text, droppedBytes } = s.tail.result();
795
549
  const droppedBeforeCursor = Math.max(0, droppedBytes - s.cursor);
796
550
  const startByte = Math.max(s.cursor, droppedBytes);
797
551
  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).
805
552
  const reencoded = Buffer.from(text, "utf8");
806
553
  const delta = reencoded.length - (s.total - droppedBytes);
807
554
  const inc = startInTail <= 0 ? text : reencoded.subarray(startInTail + delta).toString("utf8");
@@ -818,10 +565,10 @@ export class RemoteContainerExecutionEnv {
818
565
  };
819
566
  },
820
567
  async kill(state) {
821
- await state.handle.kill().catch(() => { }); // idempotent: no-op if already settled
568
+ await state.handle.kill().catch(() => { });
822
569
  },
823
570
  async disposeOne(state) {
824
- await state.handle.kill().catch(() => { }); // best-effort; the VM destroy reaps anything that survives
571
+ await state.handle.kill().catch(() => { });
825
572
  },
826
573
  };
827
574
  }
@@ -835,11 +582,8 @@ export class RemoteContainerExecutionEnv {
835
582
  return this.bgManager.kill(shellId);
836
583
  }
837
584
  disposeBackgroundShells(opts) {
838
- // 签名统一带 opts(core 尾调 {except: keepAlive})。隔离 env 的留活是名义的——沙箱拆机进程必然随之
839
- // 消亡(destroy 整体带走),core 的 envDying settle 会先给准确 killed 回执;透传保证签名不静默丢参。
840
585
  return this._bgManager ? this._bgManager.dispose(opts) : Promise.resolve();
841
586
  }
842
- // ─────────────────────────────── FileSystem ───────────────────────────────
843
587
  async absolutePath(p) {
844
588
  return ok(this.resolve(p));
845
589
  }
@@ -864,7 +608,7 @@ export class RemoteContainerExecutionEnv {
864
608
  return r;
865
609
  let lines = r.value.split(/\r?\n/);
866
610
  if (lines.length > 0 && lines[lines.length - 1] === "")
867
- lines.pop(); // drop trailing newline's empty tail
611
+ lines.pop();
868
612
  if (options?.maxLines != null)
869
613
  lines = lines.slice(0, options.maxLines);
870
614
  return ok(lines);
@@ -886,7 +630,6 @@ export class RemoteContainerExecutionEnv {
886
630
  if (!sbx.ok)
887
631
  return sbx;
888
632
  try {
889
- // E2B write creates parent directories automatically. Uint8Array → its backing ArrayBuffer slice.
890
633
  const data = typeof content === "string" ? content : toArrayBuffer(content);
891
634
  await this.rpc("files.write", (signal) => sbx.value.files.write(this.resolve(p), data, { signal }), abortSignal, this.cfg.dataTimeoutMs);
892
635
  return ok(undefined);
@@ -896,7 +639,6 @@ export class RemoteContainerExecutionEnv {
896
639
  }
897
640
  }
898
641
  async appendFile(p, content, abortSignal) {
899
- // E2B has no native append; read-modify-write (v1 single-worker → no concurrent-append race).
900
642
  const abs = this.resolve(p);
901
643
  const existing = await this.readBinaryFile(abs, abortSignal);
902
644
  let base;
@@ -939,9 +681,6 @@ export class RemoteContainerExecutionEnv {
939
681
  return { ok: false, error: this.fileError(e, this.resolve(p), abortSignal) };
940
682
  }
941
683
  }
942
- /** 见其余 shell 腿上同名方法的旁注(2026-07-25 六腿补齐):core 的敏感路径门在 `canonicalPath` 失败时退到它;
943
- * 缺它 ⇒ 合法的悬空链接让整个工具调用失败。必须返回**原始链接目标**(`readlink` 不带 `-f`)。
944
- * 本腿的文件面也走 `exec`(与 canonicalPath 同通道),所以实现方式与 shell 腿一致。 */
945
684
  async readLink(p, abortSignal) {
946
685
  const abs = this.resolve(p);
947
686
  const r = await this.exec(`readlink -- ${shellQuote(abs)}`, { abortSignal });
@@ -957,7 +696,6 @@ export class RemoteContainerExecutionEnv {
957
696
  if (!r.ok)
958
697
  return { ok: false, error: fileErrorFromExec(r.error, abs) };
959
698
  if (r.value.exitCode !== 0) {
960
- // realpath can fail for more than missing paths — classify like fileError() instead of collapsing to not_found.
961
699
  const msg = r.value.stderr.trim();
962
700
  if (/permission denied|access denied/i.test(msg))
963
701
  return { ok: false, error: new FileError("permission_denied", msg, abs) };
@@ -978,7 +716,7 @@ export class RemoteContainerExecutionEnv {
978
716
  catch (e) {
979
717
  const fe = this.fileError(e, this.resolve(p), abortSignal);
980
718
  if (fe.code === "not_found")
981
- return ok(false); // missing path is `false`, not an error (contract)
719
+ return ok(false);
982
720
  return { ok: false, error: fe };
983
721
  }
984
722
  }
@@ -987,7 +725,6 @@ export class RemoteContainerExecutionEnv {
987
725
  if (!sbx.ok)
988
726
  return sbx;
989
727
  try {
990
- // E2B makeDir creates necessary parent directories; returns false if it already exists (treated as ok).
991
728
  await this.rpc("files.makeDir", (signal) => sbx.value.files.makeDir(this.resolve(p), { signal }), options?.abortSignal);
992
729
  return ok(undefined);
993
730
  }
@@ -1001,7 +738,6 @@ export class RemoteContainerExecutionEnv {
1001
738
  return sbx;
1002
739
  const abs = this.resolve(p);
1003
740
  if (options?.recursive) {
1004
- // E2B files.remove does not guarantee recursive directory removal → shell rm -rf.
1005
741
  const flags = options.force ? "-rf" : "-r";
1006
742
  const r = await this.exec(`rm ${flags} -- ${shellQuote(abs)}`, { abortSignal: options.abortSignal });
1007
743
  if (!r.ok)
@@ -1032,17 +768,9 @@ export class RemoteContainerExecutionEnv {
1032
768
  const r = await this.exec(`mktemp ${suffixArg} ${shellQuote(`${prefix}XXXXXX`)}`, { abortSignal: options?.abortSignal });
1033
769
  return this.tempResult(r, "mktemp");
1034
770
  }
1035
- // ─────────────────────────────── internals ───────────────────────────────
1036
771
  resolve(p) {
1037
772
  return path.posix.isAbsolute(p) ? path.posix.normalize(p) : path.posix.normalize(path.posix.join(this.cwd, p));
1038
773
  }
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
- */
1046
774
  async rpc(label, op, userSignal, timeoutMs = this.cfg.rpcTimeoutMs) {
1047
775
  const deadline = new AbortController();
1048
776
  const timer = setTimeout(() => deadline.abort(), timeoutMs);
@@ -1053,55 +781,24 @@ export class RemoteContainerExecutionEnv {
1053
781
  reject(new RemoteExecutionError("timeout", `remote ${label} exceeded ${timeoutMs}ms (provider RPC hang)`));
1054
782
  }, { once: true }));
1055
783
  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).
1061
784
  return await Promise.race([op(signal), onDeadline]);
1062
785
  }
1063
786
  finally {
1064
787
  clearTimeout(timer);
1065
- combined?.dispose(); // council BUG-2: release the fallback anySignal listeners (no-op for AbortSignal.any)
788
+ combined?.dispose();
1066
789
  }
1067
790
  }
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
- */
1077
791
  commandTimeoutMs(timeoutSec) {
1078
792
  return timeoutSec != null ? timeoutSec * 1000 : this.cfg.timeoutMs;
1079
793
  }
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) {
794
+ async waitWithLiveness(handle, lastProgress, opts) {
1094
795
  let timer;
1095
796
  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.
1100
797
  const idle = new Promise((_, reject) => {
1101
798
  const arm = () => {
1102
799
  timer = setTimeout(() => {
1103
800
  if (opts?.disarmed?.())
1104
- return; // adopted: the BG wall owns the process now — never kill from the fg leg
801
+ return;
1105
802
  const idleFor = Date.now() - lastProgress();
1106
803
  if (idleFor >= this.cfg.livenessMs) {
1107
804
  void handle.kill().catch(() => { });
@@ -1109,7 +806,7 @@ export class RemoteContainerExecutionEnv {
1109
806
  reject(new RemoteExecutionError("timeout", `exec idle > ${this.cfg.livenessMs}ms (provider hang / unreachable sandbox)`));
1110
807
  }
1111
808
  else {
1112
- arm(); // progress advanced lastProgress since arming → re-arm for the now-remaining window
809
+ arm();
1113
810
  }
1114
811
  }, Math.max(this.cfg.livenessMs - (Date.now() - lastProgress()), 0));
1115
812
  };
@@ -1124,8 +821,6 @@ export class RemoteContainerExecutionEnv {
1124
821
  }
1125
822
  });
1126
823
  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.
1129
824
  return await Promise.race([opts?.waitP ?? handle.wait(), idle]);
1130
825
  }
1131
826
  finally {
@@ -1135,10 +830,7 @@ export class RemoteContainerExecutionEnv {
1135
830
  clearTimeout(wallTimer);
1136
831
  }
1137
832
  }
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}. */
1140
833
  async ensureConnected() {
1141
- // A paused (suspended) env's this.sbx is a dead handle — fail clearly rather than running fs/exec against it.
1142
834
  if (this.paused)
1143
835
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env is suspended (paused) — resume before fs/exec") };
1144
836
  if (this.sbx)
@@ -1180,16 +872,10 @@ export class RemoteContainerExecutionEnv {
1180
872
  const name = e?.name;
1181
873
  if (abortSignal?.aborted || name === "AbortError")
1182
874
  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).
1187
875
  if (e instanceof RemoteExecutionError && e.code === "timeout") {
1188
876
  this.cfg.metrics?.inc("remote_env_file_timeout_total", { provider: PROVIDER });
1189
877
  return new FileError("timeout", msg, p, e);
1190
878
  }
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".
1193
879
  if (name === "SandboxNotFoundError" || /sandbox .*not found|sandbox .*not running|sandbox was not found/i.test(msg)) {
1194
880
  return new FileError("unknown", msg, p);
1195
881
  }
@@ -1223,31 +909,15 @@ export class RemoteContainerExecutionEnv {
1223
909
  return new RemoteExecutionError(code, errMsg(e), e instanceof Error ? e : undefined);
1224
910
  }
1225
911
  }
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
- */
1236
912
  export function e2bExecutionEnvFactory(config, deps) {
1237
913
  return (ctx) => new RemoteContainerExecutionEnv({
1238
914
  ...config,
1239
- // #242:semaManaged=归属标(孤儿回收腿 e2b-orphan-reclaim.ts 按它服务端过滤;值恒 "1")。
1240
915
  metadata: { ...config.metadata, semaManaged: "1", sessionId: ctx.sessionId, ...(ctx.taskId ? { taskId: ctx.taskId } : {}) },
1241
916
  }, deps);
1242
917
  }
1243
- // ─────────────────────────────── module helpers ───────────────────────────────
1244
918
  function errMsg(e) {
1245
919
  return e instanceof Error ? e.message : String(e);
1246
920
  }
1247
- /** E2B FilesystemRequestOpts `{ signal }` spread helper (omit the key entirely when no signal). */
1248
- /** Combine signals into one that aborts when ANY input does (used by {@link RemoteContainerExecutionEnv.rpc} to
1249
- * race the caller's abort with the client-side deadline). Uses the platform `AbortSignal.any` when present
1250
- * (Node ≥20 / Bun), else a small fallback. */
1251
921
  function anySignal(signals) {
1252
922
  const ctor = AbortSignal;
1253
923
  if (typeof ctor.any === "function")
@@ -1261,11 +931,10 @@ function anySignal(signals) {
1261
931
  break;
1262
932
  }
1263
933
  s.addEventListener("abort", onAbort, { once: true });
1264
- cleanups.push(() => s.removeEventListener("abort", onAbort)); // council BUG-2: dispose() removes these on long-lived signals
934
+ cleanups.push(() => s.removeEventListener("abort", onAbort));
1265
935
  }
1266
936
  return { signal: ctrl.signal, dispose: () => cleanups.forEach((c) => c()) };
1267
937
  }
1268
- /** Copy a Uint8Array's exact bytes into a standalone ArrayBuffer (E2B write accepts ArrayBuffer). */
1269
938
  function toArrayBuffer(u) {
1270
939
  return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
1271
940
  }