@sema-agent/server 1.322.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (539) hide show
  1. package/dist/approval-hmac.d.ts +40 -0
  2. package/dist/approval-hmac.js +67 -0
  3. package/dist/approval.d.ts +31 -0
  4. package/dist/approval.js +72 -4
  5. package/dist/audit.d.ts +26 -0
  6. package/dist/audit.js +40 -1
  7. package/dist/auth-bridge.d.ts +10 -0
  8. package/dist/auth-bridge.js +47 -4
  9. package/dist/auth-keys.d.ts +19 -0
  10. package/dist/auth-keys.js +13 -0
  11. package/dist/bake-runner/main.d.ts +5 -0
  12. package/dist/bake-runner/main.js +43 -4
  13. package/dist/bake-runner/protocol.d.ts +57 -0
  14. package/dist/bake-runner/protocol.js +58 -3
  15. package/dist/bake-runner/runner.d.ts +54 -0
  16. package/dist/bake-runner/runner.js +87 -5
  17. package/dist/bench/l8/artifact.d.ts +63 -0
  18. package/dist/bench/l8/artifact.js +61 -2
  19. package/dist/bench/l8/escape.d.ts +36 -0
  20. package/dist/bench/l8/escape.js +25 -0
  21. package/dist/bench/l8/index.d.ts +14 -0
  22. package/dist/bench/l8/index.js +14 -0
  23. package/dist/bench/l8/probes.d.ts +74 -0
  24. package/dist/bench/l8/probes.js +60 -0
  25. package/dist/bench/l8/run-probes.d.ts +43 -0
  26. package/dist/bench/l8/run-probes.js +45 -2
  27. package/dist/bench/s1/arms.d.ts +126 -0
  28. package/dist/bench/s1/arms.js +188 -13
  29. package/dist/bench/s1/live-deps.d.ts +66 -0
  30. package/dist/bench/s1/live-deps.js +249 -11
  31. package/dist/bench/s1/oracle.d.ts +49 -0
  32. package/dist/bench/s1/oracle.js +36 -1
  33. package/dist/bench/s1/repair-oracle-adapter.d.ts +33 -0
  34. package/dist/bench/s1/repair-oracle-adapter.js +30 -0
  35. package/dist/bench/s1/reviewer.d.ts +38 -0
  36. package/dist/bench/s1/reviewer.js +34 -0
  37. package/dist/bench/s1/row.d.ts +77 -0
  38. package/dist/bench/s1/row.js +62 -0
  39. package/dist/bench/s1/run-firm.d.ts +47 -0
  40. package/dist/bench/s1/run-firm.js +70 -4
  41. package/dist/bench/s1/runner-ctx.d.ts +71 -0
  42. package/dist/bench/s1/runner-ctx.js +36 -0
  43. package/dist/bench/s1/tasks.d.ts +108 -0
  44. package/dist/bench/s1/tasks.js +115 -0
  45. package/dist/boot-reclaim.d.ts +19 -0
  46. package/dist/boot-reclaim.js +20 -0
  47. package/dist/brain.d.ts +25 -0
  48. package/dist/brain.js +61 -1
  49. package/dist/budget.d.ts +66 -0
  50. package/dist/budget.js +128 -4
  51. package/dist/capabilities/builtin-tools.d.ts +3 -0
  52. package/dist/capabilities/builtin-tools.js +3 -0
  53. package/dist/capabilities/center-plugins.d.ts +17 -1
  54. package/dist/capabilities/center-plugins.js +45 -4
  55. package/dist/capabilities/center-prompts.d.ts +46 -0
  56. package/dist/capabilities/center-prompts.js +57 -3
  57. package/dist/capabilities/code-review-council.d.ts +28 -0
  58. package/dist/capabilities/code-review-council.js +40 -7
  59. package/dist/capabilities/collab-wire.d.ts +2 -0
  60. package/dist/capabilities/collab-workflows.d.ts +7 -0
  61. package/dist/capabilities/collab-workflows.js +44 -1
  62. package/dist/capabilities/oa-tools.d.ts +6 -0
  63. package/dist/capabilities/oa-tools.js +10 -0
  64. package/dist/capabilities/prompt.d.ts +14 -0
  65. package/dist/capabilities/prompt.js +14 -0
  66. package/dist/capabilities/prompts/code-review.d.ts +14 -0
  67. package/dist/capabilities/prompts/code-review.js +14 -0
  68. package/dist/capabilities/prompts/identity.d.ts +6 -0
  69. package/dist/capabilities/prompts/identity.js +6 -0
  70. package/dist/capabilities/prompts/team.d.ts +4 -0
  71. package/dist/capabilities/prompts/team.js +4 -0
  72. package/dist/capabilities/repo-tools.d.ts +11 -0
  73. package/dist/capabilities/repo-tools.js +8 -0
  74. package/dist/capabilities/sandbox-file-send.d.ts +95 -0
  75. package/dist/capabilities/sandbox-file-send.js +91 -3
  76. package/dist/capabilities/scenarios.d.ts +130 -0
  77. package/dist/capabilities/scenarios.js +180 -7
  78. package/dist/capabilities/select-environment-tool.d.ts +7 -0
  79. package/dist/capabilities/select-environment-tool.js +30 -0
  80. package/dist/capabilities/send-user-file-tool.d.ts +33 -0
  81. package/dist/capabilities/send-user-file-tool.js +65 -1
  82. package/dist/capabilities/skills.d.ts +29 -0
  83. package/dist/capabilities/skills.js +13 -2
  84. package/dist/capabilities/team.d.ts +22 -0
  85. package/dist/capabilities/team.js +22 -2
  86. package/dist/capabilities/tool-defer.d.ts +26 -0
  87. package/dist/capabilities/tool-defer.js +3 -0
  88. package/dist/config-center/apply-effective.d.ts +65 -0
  89. package/dist/config-center/apply-effective.js +204 -12
  90. package/dist/config-center/http-client.d.ts +34 -0
  91. package/dist/config-center/http-client.js +68 -0
  92. package/dist/config-center/restart-signal.d.ts +22 -0
  93. package/dist/config-center/restart-signal.js +39 -0
  94. package/dist/config-center/skills-mcp.d.ts +20 -1
  95. package/dist/config-center/skills-mcp.js +42 -7
  96. package/dist/config-center/types.d.ts +92 -1
  97. package/dist/config-lkg.d.ts +12 -0
  98. package/dist/config-lkg.js +41 -0
  99. package/dist/config-provider.d.ts +78 -0
  100. package/dist/config-provider.js +165 -2
  101. package/dist/config-types.d.ts +546 -0
  102. package/dist/config.d.ts +64 -0
  103. package/dist/config.js +455 -60
  104. package/dist/degenerate-instrument.d.ts +38 -0
  105. package/dist/degenerate-instrument.js +55 -3
  106. package/dist/elicitation.d.ts +39 -0
  107. package/dist/elicitation.js +87 -3
  108. package/dist/env-facts.d.ts +61 -0
  109. package/dist/env-facts.js +73 -7
  110. package/dist/fleet/fleet-bus.d.ts +185 -0
  111. package/dist/fleet/fleet-bus.js +333 -27
  112. package/dist/fleet/subagent-tail-bus.d.ts +11 -0
  113. package/dist/fleet/subagent-tail-bus.js +33 -2
  114. package/dist/fleet-client.d.ts +38 -0
  115. package/dist/fleet-client.js +44 -9
  116. package/dist/fleet-lease.d.ts +46 -0
  117. package/dist/fleet-lease.js +73 -5
  118. package/dist/hooks/branch-transcript.d.ts +8 -0
  119. package/dist/hooks/branch-transcript.js +33 -0
  120. package/dist/hooks/cc-stop-prompt.d.ts +42 -0
  121. package/dist/hooks/cc-stop-prompt.js +43 -1
  122. package/dist/hooks/hook-llm.d.ts +15 -0
  123. package/dist/hooks/hook-llm.js +47 -0
  124. package/dist/hooks/hook-runner.d.ts +89 -0
  125. package/dist/hooks/hook-runner.js +327 -18
  126. package/dist/http/idempotency.d.ts +31 -0
  127. package/dist/http/idempotency.js +34 -0
  128. package/dist/http/principal-gate.d.ts +17 -0
  129. package/dist/http/principal-gate.js +35 -3
  130. package/dist/http/route-ctx.d.ts +36 -0
  131. package/dist/http/routes/approvals-assistant.d.ts +25 -0
  132. package/dist/http/routes/approvals-assistant.js +253 -23
  133. package/dist/http/routes/attachments.js +22 -4
  134. package/dist/http/routes/capabilities.d.ts +7 -0
  135. package/dist/http/routes/capabilities.js +202 -1
  136. package/dist/http/routes/fleet.d.ts +6 -0
  137. package/dist/http/routes/fleet.js +94 -5
  138. package/dist/http/routes/images.d.ts +17 -0
  139. package/dist/http/routes/images.js +222 -14
  140. package/dist/http/routes/leader.d.ts +7 -0
  141. package/dist/http/routes/leader.js +10 -0
  142. package/dist/http/routes/memory-policy.d.ts +6 -0
  143. package/dist/http/routes/memory-policy.js +38 -6
  144. package/dist/http/routes/notify-wake.d.ts +10 -0
  145. package/dist/http/routes/notify-wake.js +19 -3
  146. package/dist/http/routes/observability.d.ts +6 -0
  147. package/dist/http/routes/observability.js +19 -2
  148. package/dist/http/routes/runs.d.ts +15 -0
  149. package/dist/http/routes/runs.js +464 -41
  150. package/dist/http/routes/session-sync.d.ts +15 -0
  151. package/dist/http/routes/session-sync.js +229 -19
  152. package/dist/http/routes/sessions-list.d.ts +6 -0
  153. package/dist/http/routes/sessions-list.js +35 -4
  154. package/dist/http/routes/sessions.d.ts +14 -0
  155. package/dist/http/routes/sessions.js +176 -31
  156. package/dist/http/routes/side-query.d.ts +8 -0
  157. package/dist/http/routes/side-query.js +26 -1
  158. package/dist/http/routes/tasks.d.ts +18 -0
  159. package/dist/http/routes/tasks.js +501 -38
  160. package/dist/http/routes/trace-usage.d.ts +10 -0
  161. package/dist/http/routes/trace-usage.js +66 -14
  162. package/dist/http/routes/workflows.d.ts +12 -0
  163. package/dist/http/routes/workflows.js +180 -19
  164. package/dist/http/run-meta.d.ts +11 -0
  165. package/dist/http/run-meta.js +6 -0
  166. package/dist/http/send.d.ts +19 -0
  167. package/dist/http/send.js +11 -0
  168. package/dist/http/server.d.ts +255 -0
  169. package/dist/http/server.js +921 -75
  170. package/dist/http/sse-log.d.ts +15 -0
  171. package/dist/http/sse-log.js +16 -0
  172. package/dist/http/tar.d.ts +9 -0
  173. package/dist/http/tar.js +21 -5
  174. package/dist/http/wire-gate.d.ts +9 -0
  175. package/dist/http/wire-gate.js +9 -0
  176. package/dist/http/wire-types.d.ts +187 -0
  177. package/dist/http/workspace-content.d.ts +8 -0
  178. package/dist/http/workspace-content.js +10 -0
  179. package/dist/images/bake-validate.d.ts +63 -0
  180. package/dist/images/bake-validate.js +71 -1
  181. package/dist/images/manifest.d.ts +19 -0
  182. package/dist/images/manifest.js +6 -1
  183. package/dist/index.d.ts +1 -5
  184. package/dist/index.js +10 -5
  185. package/dist/key-resolver.d.ts +23 -0
  186. package/dist/key-resolver.js +27 -2
  187. package/dist/leader/diffout.d.ts +23 -0
  188. package/dist/leader/diffout.js +8 -1
  189. package/dist/leader/diffup.d.ts +32 -0
  190. package/dist/leader/diffup.js +43 -0
  191. package/dist/leader/endpoint.d.ts +20 -0
  192. package/dist/leader/endpoint.js +25 -2
  193. package/dist/leader/fanout.d.ts +60 -0
  194. package/dist/leader/fanout.js +29 -3
  195. package/dist/leader/grader-env-factory.d.ts +74 -0
  196. package/dist/leader/grader-env-factory.js +62 -3
  197. package/dist/leader/leader.d.ts +97 -0
  198. package/dist/leader/leader.js +151 -4
  199. package/dist/leader/merge.d.ts +94 -0
  200. package/dist/leader/merge.js +112 -9
  201. package/dist/leader/planner.d.ts +52 -0
  202. package/dist/leader/planner.js +47 -3
  203. package/dist/leader/repair-oracle.d.ts +61 -0
  204. package/dist/leader/repair-oracle.js +59 -1
  205. package/dist/leader/repair-wire.d.ts +78 -0
  206. package/dist/leader/repair-wire.js +79 -2
  207. package/dist/leader/wire.d.ts +79 -0
  208. package/dist/leader/wire.js +255 -8
  209. package/dist/lsp/e2b-bridge.d.ts +16 -0
  210. package/dist/lsp/e2b-bridge.js +32 -4
  211. package/dist/lsp/e2b-manager.d.ts +10 -0
  212. package/dist/lsp/e2b-manager.js +18 -2
  213. package/dist/lsp/lsp-frames.d.ts +12 -0
  214. package/dist/lsp/lsp-frames.js +12 -0
  215. package/dist/lsp/manager.d.ts +12 -0
  216. package/dist/lsp/manager.js +35 -2
  217. package/dist/lsp/types.d.ts +10 -0
  218. package/dist/lsp/ws-transport.d.ts +12 -0
  219. package/dist/lsp/ws-transport.js +53 -5
  220. package/dist/lsp-evict.d.ts +14 -0
  221. package/dist/lsp-evict.js +15 -1
  222. package/dist/main.js +2132 -96
  223. package/dist/memory-export.d.ts +1 -0
  224. package/dist/memory-export.js +4 -0
  225. package/dist/memory-scope.d.ts +49 -0
  226. package/dist/memory-scope.js +87 -4
  227. package/dist/memory-sync-client.d.ts +24 -0
  228. package/dist/memory-sync-client.js +41 -2
  229. package/dist/memory-sync.d.ts +23 -0
  230. package/dist/memory-sync.js +70 -1
  231. package/dist/model-select.d.ts +39 -0
  232. package/dist/model-select.js +42 -1
  233. package/dist/observability/cost-quota.d.ts +20 -0
  234. package/dist/observability/cost-quota.js +4 -0
  235. package/dist/observability/cost-taxonomy.d.ts +45 -0
  236. package/dist/observability/cost-taxonomy.js +34 -0
  237. package/dist/observability/logger.d.ts +1 -0
  238. package/dist/observability/logger.js +6 -0
  239. package/dist/observability/metrics.d.ts +13 -0
  240. package/dist/observability/metrics.js +63 -0
  241. package/dist/observability/otel-exporter.d.ts +9 -0
  242. package/dist/observability/otel-exporter.js +11 -3
  243. package/dist/observability/principal-context.d.ts +9 -0
  244. package/dist/observability/principal-context.js +9 -0
  245. package/dist/observability/prompt-manifest.d.ts +37 -0
  246. package/dist/observability/prompt-manifest.js +35 -1
  247. package/dist/observability/rate-limit.d.ts +10 -0
  248. package/dist/observability/rate-limit.js +1 -0
  249. package/dist/observability/tool-trace.d.ts +24 -0
  250. package/dist/observability/tool-trace.js +41 -1
  251. package/dist/orchestration/hardened-vm-runner.d.ts +107 -0
  252. package/dist/orchestration/hardened-vm-runner.js +119 -4
  253. package/dist/orchestration/hardened-vm-worker-runner.d.ts +14 -0
  254. package/dist/orchestration/hardened-vm-worker-runner.js +26 -1
  255. package/dist/orchestration/hardened-vm-worker.js +27 -0
  256. package/dist/orchestration/subagent-steer.d.ts +44 -0
  257. package/dist/orchestration/subagent-steer.js +45 -1
  258. package/dist/orchestration/workflow-agent-steer.d.ts +54 -0
  259. package/dist/orchestration/workflow-agent-steer.js +80 -1
  260. package/dist/orchestration/workflow-completion-inbox.d.ts +189 -2
  261. package/dist/orchestration/workflow-completion-inbox.js +283 -32
  262. package/dist/orchestration/workflow-notify-journal.d.ts +132 -1
  263. package/dist/orchestration/workflow-notify-journal.js +187 -13
  264. package/dist/parked-decide.d.ts +61 -0
  265. package/dist/parked-decide.js +58 -1
  266. package/dist/per-task-image.d.ts +54 -0
  267. package/dist/per-task-image.js +41 -0
  268. package/dist/plan-cache-probe.d.ts +35 -0
  269. package/dist/plan-cache-probe.js +18 -3
  270. package/dist/plugins/approval-exemption-store.d.ts +7 -0
  271. package/dist/plugins/approval-exemption-store.js +28 -2
  272. package/dist/plugins/approval-store-sql.d.ts +64 -0
  273. package/dist/plugins/approval-store-sql.js +32 -0
  274. package/dist/plugins/background-agent-store-sql.d.ts +55 -0
  275. package/dist/plugins/background-agent-store-sql.js +91 -4
  276. package/dist/plugins/background-shell-support.d.ts +98 -0
  277. package/dist/plugins/background-shell-support.js +117 -13
  278. package/dist/plugins/blob-backend.d.ts +104 -0
  279. package/dist/plugins/blob-backend.js +163 -6
  280. package/dist/plugins/breaker-state-sql.d.ts +65 -1
  281. package/dist/plugins/breaker-state-sql.js +46 -8
  282. package/dist/plugins/caching-session-store.d.ts +42 -0
  283. package/dist/plugins/caching-session-store.js +63 -3
  284. package/dist/plugins/checkpoint-store-sql.d.ts +179 -0
  285. package/dist/plugins/checkpoint-store-sql.js +239 -11
  286. package/dist/plugins/file-outcome-sink.d.ts +4 -0
  287. package/dist/plugins/file-outcome-sink.js +9 -0
  288. package/dist/plugins/file-resume-anchor-store.d.ts +20 -0
  289. package/dist/plugins/file-resume-anchor-store.js +43 -4
  290. package/dist/plugins/file-run-store.d.ts +90 -0
  291. package/dist/plugins/file-run-store.js +145 -13
  292. package/dist/plugins/file-snapshot-store-sql.d.ts +133 -1
  293. package/dist/plugins/file-snapshot-store-sql.js +178 -10
  294. package/dist/plugins/file-workflow-journal-store.d.ts +10 -0
  295. package/dist/plugins/file-workflow-journal-store.js +10 -0
  296. package/dist/plugins/fork-routing-session-store.d.ts +47 -0
  297. package/dist/plugins/fork-routing-session-store.js +51 -3
  298. package/dist/plugins/host-platform.d.ts +69 -0
  299. package/dist/plugins/host-platform.js +81 -2
  300. package/dist/plugins/image-bake-store-sql.d.ts +136 -1
  301. package/dist/plugins/image-bake-store-sql.js +241 -7
  302. package/dist/plugins/image-index-sql.d.ts +46 -0
  303. package/dist/plugins/image-index-sql.js +113 -4
  304. package/dist/plugins/k8s-bg-scripts.d.ts +34 -0
  305. package/dist/plugins/k8s-bg-scripts.js +88 -4
  306. package/dist/plugins/k8s-exec-protocol.d.ts +33 -0
  307. package/dist/plugins/k8s-exec-protocol.js +41 -0
  308. package/dist/plugins/local-checkpoint-store.d.ts +40 -0
  309. package/dist/plugins/local-checkpoint-store.js +86 -7
  310. package/dist/plugins/local-session-store.d.ts +157 -0
  311. package/dist/plugins/local-session-store.js +289 -34
  312. package/dist/plugins/local-task-attachment-store.js +14 -2
  313. package/dist/plugins/mailbox-store-sql.d.ts +38 -0
  314. package/dist/plugins/mailbox-store-sql.js +39 -6
  315. package/dist/plugins/memory-engine-pg.d.ts +30 -0
  316. package/dist/plugins/memory-engine-pg.js +122 -4
  317. package/dist/plugins/memory-engine-tidb.d.ts +26 -0
  318. package/dist/plugins/memory-engine-tidb.js +115 -5
  319. package/dist/plugins/memory-engine-vector-util.d.ts +5 -0
  320. package/dist/plugins/memory-engine-vector-util.js +5 -0
  321. package/dist/plugins/memory-resume-anchor-store.d.ts +8 -0
  322. package/dist/plugins/memory-resume-anchor-store.js +17 -0
  323. package/dist/plugins/memory-run-store.d.ts +63 -0
  324. package/dist/plugins/memory-run-store.js +73 -8
  325. package/dist/plugins/memory-session-policy-store.d.ts +17 -0
  326. package/dist/plugins/memory-session-policy-store.js +17 -0
  327. package/dist/plugins/memory-sync-store-pg.d.ts +42 -0
  328. package/dist/plugins/memory-sync-store-pg.js +45 -2
  329. package/dist/plugins/memory-sync-store-tidb.d.ts +9 -0
  330. package/dist/plugins/memory-sync-store-tidb.js +31 -1
  331. package/dist/plugins/outcome-ledger-sql.d.ts +63 -0
  332. package/dist/plugins/outcome-ledger-sql.js +97 -3
  333. package/dist/plugins/pg-approval-store.d.ts +7 -0
  334. package/dist/plugins/pg-approval-store.js +7 -0
  335. package/dist/plugins/pg-breaker-state.d.ts +6 -0
  336. package/dist/plugins/pg-breaker-state.js +6 -0
  337. package/dist/plugins/pg-checkpoint-store.d.ts +8 -0
  338. package/dist/plugins/pg-checkpoint-store.js +8 -0
  339. package/dist/plugins/pg-cost-quota.d.ts +17 -0
  340. package/dist/plugins/pg-cost-quota.js +6 -0
  341. package/dist/plugins/pg-file-snapshot-store.d.ts +6 -0
  342. package/dist/plugins/pg-file-snapshot-store.js +6 -0
  343. package/dist/plugins/pg-image-bake.d.ts +9 -0
  344. package/dist/plugins/pg-image-bake.js +9 -0
  345. package/dist/plugins/pg-image-index.d.ts +9 -0
  346. package/dist/plugins/pg-image-index.js +9 -0
  347. package/dist/plugins/pg-outcome-ledger.d.ts +9 -0
  348. package/dist/plugins/pg-outcome-ledger.js +9 -0
  349. package/dist/plugins/pg-pool.d.ts +50 -0
  350. package/dist/plugins/pg-pool.js +78 -0
  351. package/dist/plugins/pg-query.d.ts +11 -0
  352. package/dist/plugins/pg-rate-limiter.d.ts +23 -0
  353. package/dist/plugins/pg-rate-limiter.js +12 -2
  354. package/dist/plugins/pg-resume-anchor-store.d.ts +5 -0
  355. package/dist/plugins/pg-resume-anchor-store.js +5 -0
  356. package/dist/plugins/pg-run-store.d.ts +7 -0
  357. package/dist/plugins/pg-run-store.js +7 -0
  358. package/dist/plugins/pg-safe-json.d.ts +13 -0
  359. package/dist/plugins/pg-safe-json.js +40 -4
  360. package/dist/plugins/pg-session-policy-store.d.ts +5 -0
  361. package/dist/plugins/pg-session-policy-store.js +5 -0
  362. package/dist/plugins/pg-session-storage.d.ts +72 -0
  363. package/dist/plugins/pg-session-storage.js +152 -20
  364. package/dist/plugins/pg-session-store.d.ts +10 -0
  365. package/dist/plugins/pg-session-store.js +10 -0
  366. package/dist/plugins/pg-tool-result-store.d.ts +7 -0
  367. package/dist/plugins/pg-tool-result-store.js +7 -0
  368. package/dist/plugins/pg-workflow-journal-store.d.ts +7 -0
  369. package/dist/plugins/pg-workflow-journal-store.js +7 -0
  370. package/dist/plugins/pg-workflow-run-store.d.ts +7 -0
  371. package/dist/plugins/pg-workflow-run-store.js +7 -0
  372. package/dist/plugins/posix-shell-fs.d.ts +3 -0
  373. package/dist/plugins/posix-shell-fs.js +31 -1
  374. package/dist/plugins/remote-env-adb.d.ts +25 -0
  375. package/dist/plugins/remote-env-adb.js +95 -12
  376. package/dist/plugins/remote-env-e2b.d.ts +146 -0
  377. package/dist/plugins/remote-env-e2b.js +371 -40
  378. package/dist/plugins/remote-env-file-error.d.ts +34 -0
  379. package/dist/plugins/remote-env-file-error.js +37 -0
  380. package/dist/plugins/remote-env-host.d.ts +151 -0
  381. package/dist/plugins/remote-env-host.js +468 -70
  382. package/dist/plugins/remote-env-k8s.d.ts +135 -0
  383. package/dist/plugins/remote-env-k8s.js +318 -32
  384. package/dist/plugins/remote-env-local-docker.d.ts +65 -0
  385. package/dist/plugins/remote-env-local-docker.js +149 -24
  386. package/dist/plugins/remote-env-ssh.d.ts +35 -0
  387. package/dist/plugins/remote-env-ssh.js +117 -20
  388. package/dist/plugins/remote-scratchpad.d.ts +38 -0
  389. package/dist/plugins/remote-scratchpad.js +32 -2
  390. package/dist/plugins/remote-shell.d.ts +22 -0
  391. package/dist/plugins/remote-shell.js +27 -1
  392. package/dist/plugins/resume-anchor-store-sql.d.ts +28 -0
  393. package/dist/plugins/resume-anchor-store-sql.js +11 -0
  394. package/dist/plugins/roster-store-sql.d.ts +27 -0
  395. package/dist/plugins/roster-store-sql.js +54 -0
  396. package/dist/plugins/run-store-sql.d.ts +194 -0
  397. package/dist/plugins/run-store-sql.js +210 -9
  398. package/dist/plugins/s3-presign.d.ts +20 -0
  399. package/dist/plugins/s3-presign.js +49 -2
  400. package/dist/plugins/scheduler-support.d.ts +12 -0
  401. package/dist/plugins/scheduler-support.js +75 -3
  402. package/dist/plugins/send-file-ledger.d.ts +15 -0
  403. package/dist/plugins/send-file-ledger.js +53 -4
  404. package/dist/plugins/send-user-file.d.ts +77 -0
  405. package/dist/plugins/send-user-file.js +92 -4
  406. package/dist/plugins/session-policy-store-sql.d.ts +15 -0
  407. package/dist/plugins/session-policy-store-sql.js +75 -3
  408. package/dist/plugins/session-store.d.ts +45 -0
  409. package/dist/plugins/session-store.js +51 -0
  410. package/dist/plugins/sql-driver.d.ts +77 -0
  411. package/dist/plugins/sql-driver.js +4 -0
  412. package/dist/plugins/sql-escape.d.ts +8 -0
  413. package/dist/plugins/sql-escape.js +8 -0
  414. package/dist/plugins/sql-row-helpers.d.ts +25 -0
  415. package/dist/plugins/sql-row-helpers.js +25 -0
  416. package/dist/plugins/store-backend.d.ts +103 -0
  417. package/dist/plugins/store-backend.js +120 -27
  418. package/dist/plugins/store-contracts.d.ts +62 -0
  419. package/dist/plugins/store-contracts.js +3 -0
  420. package/dist/plugins/task-attachment-store.d.ts +60 -0
  421. package/dist/plugins/task-attachment-store.js +49 -3
  422. package/dist/plugins/task-list-store-sql.d.ts +25 -0
  423. package/dist/plugins/task-list-store-sql.js +21 -0
  424. package/dist/plugins/tidb-approval-store.d.ts +6 -0
  425. package/dist/plugins/tidb-approval-store.js +6 -0
  426. package/dist/plugins/tidb-breaker-state.d.ts +5 -0
  427. package/dist/plugins/tidb-breaker-state.js +5 -0
  428. package/dist/plugins/tidb-checkpoint-store.d.ts +7 -0
  429. package/dist/plugins/tidb-checkpoint-store.js +7 -0
  430. package/dist/plugins/tidb-cost-quota.d.ts +6 -0
  431. package/dist/plugins/tidb-cost-quota.js +1 -0
  432. package/dist/plugins/tidb-file-snapshot-store.d.ts +6 -0
  433. package/dist/plugins/tidb-file-snapshot-store.js +6 -0
  434. package/dist/plugins/tidb-image-bake.d.ts +8 -0
  435. package/dist/plugins/tidb-image-bake.js +8 -0
  436. package/dist/plugins/tidb-image-index.d.ts +8 -0
  437. package/dist/plugins/tidb-image-index.js +8 -0
  438. package/dist/plugins/tidb-outcome-ledger.d.ts +10 -0
  439. package/dist/plugins/tidb-outcome-ledger.js +10 -0
  440. package/dist/plugins/tidb-pool.d.ts +56 -0
  441. package/dist/plugins/tidb-pool.js +181 -1
  442. package/dist/plugins/tidb-rate-limiter.d.ts +11 -0
  443. package/dist/plugins/tidb-rate-limiter.js +5 -2
  444. package/dist/plugins/tidb-resume-anchor-store.d.ts +5 -0
  445. package/dist/plugins/tidb-resume-anchor-store.js +5 -0
  446. package/dist/plugins/tidb-run-store.d.ts +7 -0
  447. package/dist/plugins/tidb-run-store.js +7 -0
  448. package/dist/plugins/tidb-session-policy-store.d.ts +5 -0
  449. package/dist/plugins/tidb-session-policy-store.js +5 -0
  450. package/dist/plugins/tidb-session-storage.d.ts +43 -0
  451. package/dist/plugins/tidb-session-storage.js +62 -3
  452. package/dist/plugins/tidb-session-store.d.ts +142 -0
  453. package/dist/plugins/tidb-session-store.js +264 -16
  454. package/dist/plugins/tidb-tool-result-store.d.ts +5 -0
  455. package/dist/plugins/tidb-tool-result-store.js +7 -0
  456. package/dist/plugins/tidb-workflow-journal-store.d.ts +7 -0
  457. package/dist/plugins/tidb-workflow-journal-store.js +7 -0
  458. package/dist/plugins/tidb-workflow-run-store.d.ts +8 -0
  459. package/dist/plugins/tidb-workflow-run-store.js +8 -0
  460. package/dist/plugins/tool-result-store-sql.d.ts +58 -0
  461. package/dist/plugins/tool-result-store-sql.js +38 -2
  462. package/dist/plugins/web-search.d.ts +35 -0
  463. package/dist/plugins/web-search.js +45 -9
  464. package/dist/plugins/workflow-journal-limits.d.ts +10 -0
  465. package/dist/plugins/workflow-journal-limits.js +10 -0
  466. package/dist/plugins/workflow-journal-store-sql.d.ts +78 -0
  467. package/dist/plugins/workflow-journal-store-sql.js +59 -3
  468. package/dist/plugins/workflow-run-store-sql.d.ts +84 -0
  469. package/dist/plugins/workflow-run-store-sql.js +84 -6
  470. package/dist/plugins/worktree-isolation.d.ts +45 -0
  471. package/dist/plugins/worktree-isolation.js +105 -6
  472. package/dist/plugins/write-behind-counter.d.ts +56 -1
  473. package/dist/plugins/write-behind-counter.js +53 -14
  474. package/dist/principal-jwt.d.ts +24 -0
  475. package/dist/principal-jwt.js +60 -5
  476. package/dist/project-memory.d.ts +22 -0
  477. package/dist/project-memory.js +144 -15
  478. package/dist/prompts-domain-validate.d.ts +28 -0
  479. package/dist/prompts-domain-validate.js +42 -1
  480. package/dist/question.d.ts +33 -0
  481. package/dist/question.js +69 -2
  482. package/dist/resource-suspend.d.ts +21 -0
  483. package/dist/resource-suspend.js +16 -0
  484. package/dist/router/route-orchestration.d.ts +78 -0
  485. package/dist/router/route-orchestration.js +77 -0
  486. package/dist/run-local.d.ts +32 -0
  487. package/dist/run-local.js +172 -8
  488. package/dist/runs.d.ts +189 -4
  489. package/dist/runs.js +465 -24
  490. package/dist/runtime-caps-resolver.d.ts +106 -0
  491. package/dist/runtime-caps-resolver.js +89 -4
  492. package/dist/runtime-governance.d.ts +84 -0
  493. package/dist/runtime-governance.js +127 -1
  494. package/dist/sandbox-pkg-source.d.ts +23 -0
  495. package/dist/sandbox-pkg-source.js +37 -0
  496. package/dist/sealed-key.d.ts +54 -0
  497. package/dist/sealed-key.js +68 -3
  498. package/dist/security.d.ts +108 -0
  499. package/dist/security.js +145 -6
  500. package/dist/sema-registry.d.ts +21 -0
  501. package/dist/sema-registry.js +21 -0
  502. package/dist/session-leaf-bus.d.ts +31 -0
  503. package/dist/session-leaf-bus.js +32 -0
  504. package/dist/session-sync-content.d.ts +46 -0
  505. package/dist/session-sync-content.js +47 -1
  506. package/dist/session-sync-kernel.d.ts +89 -0
  507. package/dist/session-sync-kernel.js +48 -3
  508. package/dist/session-sync.d.ts +125 -0
  509. package/dist/session-sync.js +181 -8
  510. package/dist/session-titler.d.ts +26 -0
  511. package/dist/session-titler.js +41 -7
  512. package/dist/session-watch.d.ts +53 -1
  513. package/dist/session-watch.js +73 -9
  514. package/dist/sighup-idle.d.ts +30 -0
  515. package/dist/sighup-idle.js +12 -1
  516. package/dist/spec-fields.d.ts +91 -3
  517. package/dist/spec-fields.js +116 -4
  518. package/dist/task-cwd.d.ts +50 -0
  519. package/dist/task-cwd.js +68 -1
  520. package/dist/task-mcp.d.ts +47 -0
  521. package/dist/task-mcp.js +29 -0
  522. package/dist/task-settings.d.ts +175 -0
  523. package/dist/task-settings.js +226 -11
  524. package/dist/task-workflow.d.ts +67 -0
  525. package/dist/task-workflow.js +75 -8
  526. package/dist/tool-approval.d.ts +91 -0
  527. package/dist/tool-approval.js +215 -0
  528. package/dist/trace/artifacts.d.ts +9 -0
  529. package/dist/trace/artifacts.js +30 -5
  530. package/dist/trace/core-keyset-guard.d.ts +18 -0
  531. package/dist/trace/ledger-sink.d.ts +43 -0
  532. package/dist/trace/ledger-sink.js +64 -5
  533. package/dist/trace/project.d.ts +133 -0
  534. package/dist/trace/project.js +189 -2
  535. package/dist/trace/redact.d.ts +19 -0
  536. package/dist/trace/redact.js +66 -9
  537. package/dist/usage-analytics.d.ts +26 -0
  538. package/dist/usage-analytics.js +26 -3
  539. package/package.json +1 -1
@@ -1,3 +1,36 @@
1
+ /**
2
+ * `RemoteHostExecutionEnv` — the **TOC `host` exec adapter** (DUAL-MODE-DESIGN §5). Runs
3
+ * commands DIRECTLY on THIS machine, in NO container — the TOC ("take-over-this-computer") default, exactly
4
+ * like a local coding agent operating on the user's own box. **Isolation = none.**
5
+ *
6
+ * It is a PEER adapter to the E2B / SSH / k8s ones (not chained): all four implement the SAME core seam
7
+ * `RemoteExecutionEnv` (core src/core/remote-env.ts, design/48 §5) so the agent's "hand" plugs into any of
8
+ * them via the deployment-level `ExecutionEnvFactory`. The discriminator that selects between them is the
9
+ * `remoteExec.provider` superset — this adapter is `provider:"host"` (DUAL-MODE-DESIGN §5).
10
+ *
11
+ * 🔴 **`host` targets the operator's OWN machine, not a disposable sandbox** (DUAL-MODE-DESIGN §5):
12
+ * - `capabilities = { isolation: false, suspendable: false }` — there is NO container boundary and NO
13
+ * snapshot. Side effects are PERMANENT and visible to the whole machine. The orchestrator/leader MUST
14
+ * tighten autonomy + the design/37 policy gate + HITL on a non-isolated env (design/53 zero-trust) —
15
+ * identical posture to the SSH adapter, which also runs on a real (un-isolated) system.
16
+ * - **NO network restriction** — this is the user's own machine; the agent has the same network the user
17
+ * does (that is the whole point of the TOC mode).
18
+ * - `suspendVM`/`resumeVM`/`postResumeInit` → typed `"unsupported"` (a live host process tree isn't
19
+ * snapshotable — durable suspend N/A). `reconnect` is a no-op success (the "connection" is the local
20
+ * process; nothing to re-establish).
21
+ * - `destroy()` / `cleanup()` **`rm -rf` the per-task working directory** (the workspace this adapter
22
+ * created), NOT the machine. Anything the agent created OUTSIDE the workspace persists (that is the
23
+ * operator's/gate's concern — there is no isolation to undo).
24
+ *
25
+ * **never-throw invariant:** every base `ExecutionEnv` FileSystem/Shell method encodes failure in its
26
+ * returned `Result` (never throws). `execStream`'s iterator MAY throw `RemoteExecutionError` mid-stream
27
+ * before an `exit` chunk (the seam contract, OutputChunk). A non-zero command exit is a NORMAL value
28
+ * (`{ ok:true, exitCode }`), never an error.
29
+ *
30
+ * Platforms: macOS + Linux (POSIX shell). Forward-compatible with the `remoteExec` superset: this file also
31
+ * scaffolds the `local-docker` / `remote-docker` providers' factory entry points (typed stubs that throw a
32
+ * clear "not yet implemented" — explicit fast-follow per DUAL-MODE-DESIGN §5; NOT faked green).
33
+ */
1
34
  import { spawn } from "node:child_process";
2
35
  import os from "node:os";
3
36
  import path from "node:path";
@@ -6,24 +39,37 @@ import { randomBytes } from "node:crypto";
6
39
  import { openSync, closeSync, readSync, statSync, truncateSync, unlinkSync, mkdirSync, rmdirSync, mkdtempSync, existsSync, realpathSync } from "node:fs";
7
40
  import { StringDecoder } from "node:string_decoder";
8
41
  import { armPipeDestroyGrace, numEnvOr } from "./remote-shell.js";
42
+ // design/158 B4 (lens4 N8): the two host-lane kill switches were the negative names `HOST_BG_DISABLED` /
43
+ // `HOST_EXEC_SPOOL_DISABLED`, parsed inline. Their canonical names are now positive (`HOST_BG_ENABLED` /
44
+ // `HOST_EXEC_SPOOL_ENABLED`, default ON) and resolved by the shared config helpers, which keep the retired
45
+ // names working for one deprecation window and register both in the boot polarity table.
9
46
  import { hostBackgroundShellEnabled, hostExecSpoolEnabled } from "../config.js";
10
47
  import { resolveHostShell, hostShell, spawnGroupOptions, killTreeHard, killTreeSoft, collapseWin32EnvKeys } from "./host-platform.js";
11
48
  import { BackgroundShellManager, seedMemStream, feedMemStream, drainMemStream } from "./background-shell-support.js";
12
49
  import { FileError, ExecutionError, RemoteExecutionError, scrubSecretEnv, RollingTailBuffer, markTruncated, SchedulerError, BackgroundShellError, } from "@sema-agent/core";
13
50
  const PROVIDER = "host";
51
+ /** Default per-command wall-clock when the caller passes none (NOT a hang detector — host commands rarely hang). */
14
52
  const DEFAULT_COMMAND_TIMEOUT_MS = 30 * 60_000;
53
+ // ─── design/103 host-lane background shell: TOC's run_in_background / BashOutput / KillShell ───
54
+ // host driver = design/103 §5.2 path ①: a `detached` child (its OWN process group via `detached:true` → pgid ===
55
+ // child.pid) writes stdout/stderr DIRECTLY to two local files; poll re-reads each by byte cursor; kill/dispose reap
56
+ // the whole group with `process.kill(-pid)`. The BackgroundShellManager owns every red-line (opaque id / owned-id
57
+ // isolation / maxConcurrent / timeout hard wall / dispose); this driver only writes the 4 transport primitives.
15
58
  const HOST_BG_MAX_CONCURRENT = numEnvOr("HOST_BG_MAX_CONCURRENT", 8, 1);
16
59
  const HOST_BG_DEFAULT_TIMEOUT_SEC = numEnvOr("HOST_BG_DEFAULT_TIMEOUT_SEC", 300, 1);
17
60
  const HOST_BG_MAX_TIMEOUT_SEC = numEnvOr("HOST_BG_MAX_TIMEOUT_SEC", 1800, HOST_BG_DEFAULT_TIMEOUT_SEC);
18
- const HOST_BG_READ_CAP = numEnvOr("HOST_BG_READ_CAP", 1024 * 1024, 64 * 1024);
19
- const HOST_BG_FILE_CAP = numEnvOr("HOST_BG_FILE_CAP", 64 * 1024 * 1024, HOST_BG_READ_CAP);
20
- const HOST_BG_KILL_GRACE_MS = numEnvOr("HOST_BG_KILL_GRACE_MS", 1000, 0);
61
+ const HOST_BG_READ_CAP = numEnvOr("HOST_BG_READ_CAP", 1024 * 1024, 64 * 1024); // per-poll incremental read ceiling
62
+ const HOST_BG_FILE_CAP = numEnvOr("HOST_BG_FILE_CAP", 64 * 1024 * 1024, HOST_BG_READ_CAP); // disk guard: reap a runaway
63
+ const HOST_BG_KILL_GRACE_MS = numEnvOr("HOST_BG_KILL_GRACE_MS", 1000, 0); // SIGTERM → grace → SIGKILL (no host isolation)
64
+ /** Per-stream memory-mode drain cap: an un-polled adopted child cannot grow the worker heap without bound (the
65
+ * host lane has no VM to contain it). Drop-oldest beyond this, counting dropped bytes → surfaced as `truncated`. */
21
66
  const HOST_BG_MEM_CAP = numEnvOr("HOST_BG_MEM_CAP", 8 * 1024 * 1024, 64 * 1024);
22
67
  const ok = (value) => ({ ok: true, value });
23
68
  const unsupported = (op) => ({
24
69
  ok: false,
25
70
  error: new RemoteExecutionError("unsupported", `${op} is not supported on a host target (the operator's own machine — no container boundary, not snapshotable; capabilities.suspendable=false)`),
26
71
  });
72
+ // The cap shape an env without a backend advertises — `supported:false` makes `hasScheduler` false (INERT, no tools).
27
73
  const UNSUPPORTED_SCHEDULER_CAPS = {
28
74
  supported: false,
29
75
  maxScheduledPerScope: 0,
@@ -32,7 +78,11 @@ const UNSUPPORTED_SCHEDULER_CAPS = {
32
78
  maxScheduledHorizonSec: 0,
33
79
  };
34
80
  export class RemoteHostExecutionEnv {
81
+ /** design/61 §9 / DUAL-MODE-DESIGN §5: a `host` env is the operator's real machine — no isolation, no snapshot. */
35
82
  capabilities = { isolation: false, suspendable: false };
83
+ /** R7 自唤醒: structural delegation to the injected scheduler backend. `hasScheduler(env)` checks BOTH the method
84
+ * presence (always, since we implement the interface) AND `schedulerCapabilities.supported===true` — which is only
85
+ * true when a backend was injected, so a host env without one stays INERT (the four scheduler tools don't mount). */
36
86
  get schedulerCapabilities() {
37
87
  return this.cfg.scheduler?.schedulerCapabilities ?? UNSUPPORTED_SCHEDULER_CAPS;
38
88
  }
@@ -51,30 +101,45 @@ export class RemoteHostExecutionEnv {
51
101
  ? this.cfg.scheduler.list(ctx)
52
102
  : Promise.resolve({ ok: false, error: new SchedulerError("unsupported", "scheduler not enabled on this worker") });
53
103
  }
104
+ /** Working directory; relative paths resolve against it (ExecutionEnv contract). Starts at the workspace root. */
54
105
  cwd;
55
106
  workspaceDir;
56
107
  cfg;
108
+ /** Workspace created on disk (lazily, on first use). */
57
109
  prepared = false;
110
+ /** Terminal once {@link destroy} ran — guards fs/exec from running against a removed workspace. */
58
111
  destroyed = false;
112
+ /** [1116] 五轮复审:fg/bg spool 根=tmpdir 私有目录(core bgSpoolDir 同形)。workspace 是命令可写区,
113
+ * `git clean -fdx` 类清理会 unlink 活跃 spool(子进程经继承 fd 对 unlinked inode 继续写,而父进程
114
+ * 按路径 statSync/read 静默断=输出丢失+轮转帽失效);顺带不再弄脏 user-owned workspace(TOC/CC
115
+ * persistent 形)。懒建;destroy 整树清。 */
59
116
  spoolRootDir;
117
+ /** 🔴 在飞的 fg spool 目录登记表(2026-07-25)。`destroy()` 原来只看 `bgManager.hasShells()` 就 `rm -rf`
118
+ * spool 根 —— 而**前台** exec 的 spool 也在同一个根下,子进程通过继承的 fd 继续往已 unlink 的 inode 写,
119
+ * 父进程按**路径** stat/read 于是什么都看不到 ⇒ 命令照跑、副作用照落盘,但 stdout 被静默截断,还返回
120
+ * `exitCode 0`。对模型而言那与「成功且无输出」**不可区分**。(与 core RB-73 同形:写已释放的 inode。)
121
+ * 判据与 `hasShells()` 并列:**只要还有人在读写这个根下的文件,就不删它**。 */
60
122
  liveFgSpools = new Set();
61
123
  ensureSpoolRoot() {
62
124
  if (!this.spoolRootDir)
63
125
  this.spoolRootDir = mkdtempSync(path.join(os.tmpdir(), "sema-host-spool-"));
64
126
  return this.spoolRootDir;
65
127
  }
128
+ /** True when `workspaceDir` was given (user-owned persistent dir) → destroy() must NOT delete it. */
66
129
  persistent;
130
+ /** Memoized lazy `mkdir -p` so concurrent first-use prepares the workspace exactly once. */
67
131
  preparing;
68
132
  handle;
69
133
  constructor(config = {}) {
70
134
  if (config.workspaceDir) {
135
+ // TOC/CC mode: the user's OWN dir, verbatim — no random subdir, never deleted (see destroy()).
71
136
  this.workspaceDir = path.resolve(config.workspaceDir);
72
137
  this.persistent = true;
73
138
  }
74
139
  else {
75
140
  const base = config.workspaceBase ?? os.tmpdir();
76
141
  const id = config.id ?? randomBytes(6).toString("hex");
77
- const suffix = randomBytes(4).toString("hex");
142
+ const suffix = randomBytes(4).toString("hex"); // always-unique tail so a reused id never collides
78
143
  this.workspaceDir = path.resolve(base, `sema-host-${sanitizeId(id)}-${suffix}`);
79
144
  this.persistent = false;
80
145
  }
@@ -85,12 +150,13 @@ export class RemoteHostExecutionEnv {
85
150
  this.cwd = this.workspaceDir;
86
151
  this.handle = { sandboxId: this.workspaceDir, provider: PROVIDER, mountPath: this.workspaceDir };
87
152
  }
153
+ // ─────────────────────────────── lifecycle ───────────────────────────────
88
154
  workspaceHandle() {
89
155
  return this.handle;
90
156
  }
91
157
  async connect(config) {
92
158
  if (config?.snapshotId)
93
- return unsupported("connect(snapshotId)");
159
+ return unsupported("connect(snapshotId)"); // host has no snapshots
94
160
  if (this.destroyed)
95
161
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env already destroyed") };
96
162
  if (config?.abortSignal?.aborted)
@@ -102,6 +168,7 @@ export class RemoteHostExecutionEnv {
102
168
  return { ok: false, error: new RemoteExecutionError("connect_failed", `failed to create host workspace ${this.workspaceDir}: ${errMsg(e)}`, asError(e)) };
103
169
  }
104
170
  if (config?.secrets?.length) {
171
+ // Secrets are injected per-command via cfg.env (resolved by the control plane), not at connect — record only.
105
172
  }
106
173
  return ok(this.handle);
107
174
  }
@@ -114,46 +181,72 @@ export class RemoteHostExecutionEnv {
114
181
  async postResumeInit() {
115
182
  return unsupported("postResumeInit");
116
183
  }
184
+ /** The "connection" is the local process; there is nothing to re-establish. Idempotent success (unless destroyed). */
117
185
  async reconnect(_sessionToken) {
118
186
  if (this.destroyed)
119
187
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env already destroyed") };
120
188
  return ok(this.handle);
121
189
  }
190
+ /** `rm -rf` the per-task workspace (NOT the machine). Idempotent, best-effort, never throws (cleanup contract).
191
+ *
192
+ * core 1.258.2 destroy 契约注(remote-env.d.ts):非隔离 env(host lane,capabilities.isolation
193
+ * 缺席)的 destroy = 工作区/对象级清理,**不得收割仍在跑的后台进程**——收割全权归 `disposeBackgroundShells`
194
+ * (它带 timeout/session keep-alive except 名单)。此前这里的无-except 全灭把
195
+ * core 留活过的驻留 watcher 又杀了一遍(run 尾 dispose→destroy 顺序,destroy 是第二刀)。隔离 env(容器/VM)
196
+ * 整体拆机自然带走进程,与本契约不冲突(k8s/e2b 各自 destroy 不走这条路径)。 */
122
197
  async destroy() {
123
198
  if (this.destroyed)
124
- return;
199
+ return; // idempotent — safe double-call (Runner end + reaper)
125
200
  this.destroyed = true;
201
+ // Await any in-flight prepare so a destroy-vs-first-use race can't leak the workspace dir:
202
+ // an attached .then may still flip prepared=true after we null the memo. Then rm unconditionally
203
+ // (force:true no-ops a never-created path), eliminating the prepared-flag race entirely.
126
204
  const p = this.preparing;
127
205
  this.preparing = undefined;
128
206
  try {
129
207
  await p?.catch(() => { });
130
208
  }
131
209
  finally {
210
+ // spool 根(tmpdir 私有)与 workspace 归属无关——persistent 形也要清(五轮复审迁址后属本 env 自有)。
211
+ // 六轮复审:但驻留 shell(core 1.258.2「destroy 不得收割后台进程」契约,keep-alive except 名单)与
212
+ // terminal 残余(dispose 前仍可轮询)还在读写这些文件——有任何 shell 在册即跳过整树删,逐 shell 清理
213
+ // 归 dispose 面(state.dir 单删);空根目录留给 OS tmp 周期清,量级=一个空目录。
214
+ // 判据两条并列(见 liveFgSpools 旁注):后台 shell **或**在飞的前台 exec,任一还在就不删这个根 ——
215
+ // 删了等于把它们正在写的文件从脚下抽走,而写者拿的是 fd、察觉不到,只有读者看到"空输出"。
132
216
  if (this.spoolRootDir && !this.bgManager.hasShells() && this.liveFgSpools.size === 0) {
133
217
  try {
134
218
  await fs.rm(this.spoolRootDir, { recursive: true, force: true });
135
219
  }
136
- catch { }
220
+ catch { /* best-effort */ }
137
221
  this.spoolRootDir = undefined;
138
222
  }
139
223
  if (this.persistent)
140
- return;
224
+ return; // user-owned persistent dir (TOC/CC) — NEVER rm -rf it
141
225
  try {
142
226
  await fs.rm(this.workspaceDir, { recursive: true, force: true });
143
227
  }
144
228
  catch {
229
+ /* best-effort: never throw on teardown */
145
230
  }
146
231
  }
147
232
  }
148
233
  async cleanup() {
149
234
  return this.destroy();
150
235
  }
236
+ // ─────────────────────────────── design/103 background shell (host lane) ───────────────────────────────
237
+ // Host lane = single-user TOC (the user's own box) → background shell is ON (run_in_background/BashOutput/KillShell
238
+ // auto-mount via core's `hasBackgroundShell`). `HOST_BG_ENABLED=false` turns it INERT (defence for a mis-configured
239
+ // multi-tenant host lane). caps tuned via HOST_BG_* env. The manager owns the red-lines; the driver below is local.
240
+ // A getter (NOT a field initializer) so it reads `this.cfg` which is set in the constructor — a field initializer
241
+ // runs before the constructor body and would see cfg undefined (the kata adapter hit the same ordering trap).
151
242
  get backgroundCapabilities() {
152
243
  return {
153
244
  supported: (this.cfg.backgroundShell ?? true) && hostBackgroundShellEnabled(),
154
245
  maxConcurrent: HOST_BG_MAX_CONCURRENT,
155
246
  defaultBgTimeoutSec: HOST_BG_DEFAULT_TIMEOUT_SEC,
156
247
  maxBgTimeoutSec: HOST_BG_MAX_TIMEOUT_SEC,
248
+ // core 1.207 design/116 detach (clay 排, 1.81): a running foreground exec can be ADOPTED as a background
249
+ // shell (mid-flight ctrl+b). Core's Bash tool gates the whole detach chain on this flag === true.
157
250
  supportsDetach: (this.cfg.backgroundShell ?? true) && hostBackgroundShellEnabled(),
158
251
  };
159
252
  }
@@ -165,21 +258,25 @@ export class RemoteHostExecutionEnv {
165
258
  const self = this;
166
259
  return {
167
260
  async launch(command, options, ctx) {
168
- const ready = await self.shellReady();
261
+ const ready = await self.shellReady(); // lazily prepares the workspace dir
169
262
  if (!ready.ok)
170
263
  throw new BackgroundShellError("spawn_failed", ready.error.message);
171
264
  const dir = path.join(self.ensureSpoolRoot(), "bg-" + randomBytes(8).toString("hex"));
172
265
  await fs.mkdir(dir, { recursive: true });
173
266
  const outPath = path.join(dir, "out");
174
267
  const errPath = path.join(dir, "err");
268
+ // Open the child's stdout/stderr targets; the parent closes its OWN fd copies right after spawn so file
269
+ // size/EOF reflect only the child's writes (the child keeps its inherited fds).
175
270
  const outFh = await fs.open(outPath, "w");
176
271
  const errFh = await fs.open(errPath, "w");
177
272
  let child;
178
273
  try {
179
- const sh = hostShell();
274
+ const sh = hostShell(); // Windows S1/S3: behind shellReady() above → resolved
180
275
  child = spawn(sh.shell, [...sh.args, command], {
181
276
  cwd: self.resolve(options?.cwd ?? self.cwd),
182
- env: self.mergeEnv(options?.env),
277
+ env: self.mergeEnv(options?.env), // 🔴 SAME secret-scrub as exec (design/103 §3.1) — host process.env minus secret-pattern keys
278
+ // POSIX: detached:true → child = process-group leader (pgid === pid), kill(-pid) reaps the whole
279
+ // group. win32: no POSIX group either way — windowsHide, kill side = taskkill tree (design D3).
183
280
  ...spawnGroupOptions(),
184
281
  stdio: ["ignore", outFh.fd, errFh.fd],
185
282
  });
@@ -191,10 +288,18 @@ export class RemoteHostExecutionEnv {
191
288
  throw new BackgroundShellError("spawn_failed", e instanceof Error ? e.message : String(e), e);
192
289
  }
193
290
  const pid = child.pid;
291
+ // 🔴 register close/error listeners SYNCHRONOUSLY (before any await) — a fast child must not exit before we
292
+ // listen, else onTerminal never fires and the shell would lie "running" until the timeout wall.
194
293
  if (pid != null) {
294
+ // core-baseline terminal semantics (Fable-5 review #1): ANY self-ended process is "exited" with its exit
295
+ // code (signal death synthesizes the conventional 128+signo, like core's NodeExecutionEnv + the k8s/E2B
296
+ // lanes) — a non-zero exit is a NORMAL result, NOT "failed". "failed" is reserved for spawn-level errors.
297
+ // The old non-zero→failed classification hid the exit code from the model (BashOutput only prints
298
+ // `exited(code N)`), and split the three lanes' semantics. Classify on `close` (not `exit`), also per
299
+ // core baseline — stdio settled before the status flips (with fd-passed spool files close ≈ exit anyway).
195
300
  child.on("close", (code, signal) => ctx.onTerminal(false, code ?? (signal ? 128 + (signalNumber(signal) ?? 9) : 0)));
196
- child.on("error", () => ctx.onTerminal(true));
197
- child.unref();
301
+ child.on("error", () => ctx.onTerminal(true)); // post-pid spawn error (rare)
302
+ child.unref(); // a detached background job must not keep the worker's event loop alive
198
303
  }
199
304
  await outFh.close().catch(() => { });
200
305
  await errFh.close().catch(() => { });
@@ -205,6 +310,8 @@ export class RemoteHostExecutionEnv {
205
310
  return { pid, dir, outPath, errPath, stdoutCursor: 0, stderrCursor: 0 };
206
311
  },
207
312
  async read(state) {
313
+ // ADOPT (memory) mode: drain the in-memory buffers (no files). Each read returns the queued chunks +
314
+ // resets the queue; totalBytes/droppedBytes are cursor metadata (design/103 §3.4).
208
315
  if (state.mem) {
209
316
  const o = drainMemStream(state.mem.out);
210
317
  const e = drainMemStream(state.mem.err);
@@ -215,6 +322,9 @@ export class RemoteHostExecutionEnv {
215
322
  bytesDroppedBeforeCursor: o.dropped + e.dropped,
216
323
  };
217
324
  }
325
+ // Returns the advanced cursor AND the TRUE on-disk size — the per-poll READ is capped at HOST_BG_READ_CAP,
326
+ // but the disk guard must see the real file size (a slow poller advances the cursor ≤1 MiB/poll while the
327
+ // file on the operator's real disk could already be GBs — review MED).
218
328
  const readForward = async (p, cursor, dec) => {
219
329
  let fh;
220
330
  try {
@@ -222,93 +332,134 @@ export class RemoteHostExecutionEnv {
222
332
  }
223
333
  catch {
224
334
  return { chunk: "", cursor, size: cursor };
225
- }
335
+ } // file gone (disposed) → no new bytes
226
336
  try {
227
337
  const { size } = await fh.stat();
228
338
  if (size <= cursor)
229
- return { chunk: "", cursor: size, size };
339
+ return { chunk: "", cursor: size, size }; // no growth (or shrank) → cursor tracks size
230
340
  const n = Math.min(size - cursor, HOST_BG_READ_CAP);
231
341
  const buf = Buffer.alloc(n);
232
342
  await fh.read(buf, 0, n, cursor);
343
+ // per-shell StringDecoder([1116] 批):跨段码点保状态——逐段 toString 会在段界劈出 U+FFFD。
233
344
  return { chunk: dec.write(buf), cursor: cursor + n, size };
234
345
  }
235
346
  finally {
236
347
  await fh.close().catch(() => { });
237
348
  }
238
349
  };
350
+ // File mode (launch + [1116] spool adoption): the memory-mode early-return above handled `state.mem`.
239
351
  state.decOut ??= new StringDecoder("utf8");
240
352
  state.decErr ??= new StringDecoder("utf8");
241
353
  const o = await readForward(state.outPath, state.stdoutCursor, state.decOut);
242
354
  const e = await readForward(state.errPath, state.stderrCursor, state.decErr);
243
355
  state.stdoutCursor = o.cursor;
244
356
  state.stderrCursor = e.cursor;
357
+ // Disk guard: reap a runaway by its TRUE on-disk size (the host lane has no VM isolation to contain a
358
+ // runaway output file; the BG-timeout also bounds wall-clock).
245
359
  let terminal;
246
360
  if (o.size > HOST_BG_FILE_CAP || e.size > HOST_BG_FILE_CAP) {
247
361
  try {
248
362
  killTreeHard(state.pid);
249
363
  }
250
- catch { }
364
+ catch { /* already dead */ } // disk-guard熔断 (the 5th kill site) — POSIX kill(-pid) byte-identical; win32 taskkill /F /T
251
365
  terminal = { failed: true };
252
366
  }
367
+ // F2/F3([1116] 批,二轮复审):spool adoption 携带的 fg 期文件移除字节。preSpoolBase 每拍并入
368
+ // bytesFromStart(单调不倒退);pendingDropped 只首拍并入 bytesDroppedBeforeCursor 后清零(launch
369
+ // 形两者恒 undefined→0:本地文件不 head-evict,全量保留到 dispose)。
253
370
  const preBase = state.preSpoolBase ?? 0;
254
371
  const pending = state.pendingDropped ?? 0;
255
372
  state.pendingDropped = 0;
256
373
  return {
257
374
  stdout: o.chunk,
258
375
  stderr: e.chunk,
259
- bytesFromStart: o.size + e.size + preBase,
376
+ bytesFromStart: o.size + e.size + preBase, // TRUE total from start(含 fg 已移除段;preBase 每拍恒在)
260
377
  bytesDroppedBeforeCursor: pending,
261
378
  ...(terminal ? { terminal } : {}),
262
379
  };
263
380
  },
264
381
  async kill(state) {
382
+ // host lane has NO container isolation — a SIGTERM-trapping group must still die at the hard wall. Send
383
+ // SIGTERM, then escalate to SIGKILL after a short grace (mirrors kata's `kill -TERM; sleep 1; kill -KILL`).
384
+ // This is the path for BOTH KillShell and the manager's BG-timeout wall, so the §3.6 "到点自杀" really kills.
385
+ // POSIX byte-identical: killTreeSoft = kill(-pid, SIGTERM) incl. the throw-on-dead-group → return (no
386
+ // escalation needed). win32 (design D2): soft = taskkill /T — a no-op for console trees ("can only be
387
+ // terminated forcefully"), and it never throws, so the escalation timer ALWAYS arms there: the ladder
388
+ // degrades to delay→hard-kill, which is exactly the accepted win32 semantics.
265
389
  try {
266
390
  killTreeSoft(state.pid);
267
391
  }
268
392
  catch {
269
393
  return;
270
- }
394
+ } // idempotent: already-dead group → no escalation needed
271
395
  setTimeout(() => { try {
272
396
  killTreeHard(state.pid);
273
397
  }
274
- catch { } }, HOST_BG_KILL_GRACE_MS).unref?.();
398
+ catch { /* exited within grace */ } }, HOST_BG_KILL_GRACE_MS).unref?.();
275
399
  },
276
400
  async disposeOne(state) {
277
- state.detachCleanup?.();
401
+ state.detachCleanup?.(); // adopt path: detach the extra pipe listeners (memory mode → no fds/dir)
278
402
  try {
279
403
  killTreeHard(state.pid);
280
404
  }
281
- catch { }
405
+ catch { /* already dead */ }
282
406
  if (state.dir)
283
- await fs.rm(state.dir, { recursive: true, force: true }).catch(() => { });
407
+ await fs.rm(state.dir, { recursive: true, force: true }).catch(() => { }); // file (launch) mode only
284
408
  },
285
409
  };
286
410
  }
287
411
  spawnBackground(command, options) {
288
412
  return this.bgManager.spawn(command, options);
289
413
  }
414
+ /**
415
+ * design/116 detach (core 1.207, clay 排 1.81): ADOPT a still-running FOREGROUND exec child as a background
416
+ * shell — the mid-flight ctrl+b path. The exec child writes to pipes (not files, unlike a `launch`-spawned
417
+ * bg shell), so adoption splices those live pipes into fresh bg spool files: pre-fill them with the output
418
+ * captured so far (the detach TEE, fed from the exec's FIRST chunk → TaskOutput's first read has the full
419
+ * history), then append every subsequent chunk. From then on the shell is indistinguishable from a spawned one
420
+ * (poll by cursor, kill by pgid, the BG-timeout wall applies). Returns the opaque shellId, or `undefined` when
421
+ * the live-shell limit is hit (detach refused → the exec keeps running in the foreground; NEVER a dropped process).
422
+ *
423
+ * `memOut`/`memErr` are the exec's tee-from-start buffers (Fable-5 review #3, mirrors the E2B tee): they were fed
424
+ * every chunk since process start THROUGH THE SAME per-stream StringDecoder that keeps feeding post-detach, so a
425
+ * multibyte codepoint straddling the detach instant decodes correctly (the old seed-from-fg-tail handoff decoded
426
+ * the two halves in separate passes → U+FFFD at the seam).
427
+ */
290
428
  adoptRunningExec(child, memOut, memErr) {
291
429
  const pid = child.pid;
292
430
  if (pid == null)
293
- return undefined;
431
+ return undefined; // no pid → nothing to adopt (exec keeps running / will settle normally)
432
+ // 🔴 FULLY SYNCHRONOUS (double-review, 3 findings): memory buffers, no files/fds. The tee handover + the
433
+ // live-pipe listener attach happen in ONE tick so no output is lost to an async window, and the exec-settle
434
+ // can't race the adoption (mirrors core's `adoptRunningChild`).
435
+ // 🔴 builder contract (Fable-5 review #5): everything below is NON-THROWING (listener attach + plain object
436
+ // construction). adoptSync's catch returns undefined WITHOUT undoing side effects — any future logic that can
437
+ // throw must go BEFORE the first side effect (listener/on) or clean up after itself.
294
438
  const adopted = this.bgManager.adoptSync((ctx) => {
295
439
  const mem = { out: memOut, err: memErr };
440
+ // Bound un-drained memory (no VM on the host lane to contain a runaway): drop OLDEST, count as truncated.
296
441
  const onOut = (d) => feedMemStream(mem.out, d, HOST_BG_MEM_CAP);
297
442
  const onErr = (d) => feedMemStream(mem.err, d, HOST_BG_MEM_CAP);
298
443
  child.stdout?.on("data", onOut);
299
444
  child.stderr?.on("data", onErr);
445
+ // core-baseline terminal semantics (Fable-5 review #1, same as the launch path): self-ended → "exited" with
446
+ // the exit code (signal death = 128+signo), NEVER "failed" — matches core's adoptRunningChild verbatim.
447
+ // Classify on `close` (not `exit`), also per core baseline: in pipe (adopt) mode the final data events can
448
+ // trail `exit` — flipping the status early lets a poll in that window read "exited" with the tail undrained.
300
449
  const onClose = (code, signal) => ctx.onTerminal(false, code ?? (signal ? 128 + (signalNumber(signal) ?? 9) : 0));
301
450
  const onErrEvt = () => ctx.onTerminal(true);
302
451
  child.on("close", onClose);
303
452
  child.on("error", onErrEvt);
453
+ // Race guard: the child may have exited between the detach request and adoption — a close that fired before
454
+ // the listener attached won't re-fire, so classify terminal now (mirrors core's adoptRunningChild).
304
455
  if (child.exitCode !== null || child.signalCode !== null)
305
456
  ctx.onTerminal(false, child.exitCode ?? (child.signalCode ? 128 + (signalNumber(child.signalCode) ?? 9) : 0));
306
- child.unref?.();
457
+ child.unref?.(); // a detached background job must not keep the worker's event loop alive
307
458
  const detachCleanup = () => {
308
459
  child.stdout?.off("data", onOut);
309
460
  child.stderr?.off("data", onErr);
310
461
  child.off("close", onClose);
311
- child.off("error", onErrEvt);
462
+ child.off("error", onErrEvt); // no fds to close — memory mode
312
463
  };
313
464
  return { pid, stdoutCursor: 0, stderrCursor: 0, mem, detachCleanup };
314
465
  });
@@ -321,8 +472,18 @@ export class RemoteHostExecutionEnv {
321
472
  return this.bgManager.kill(shellId);
322
473
  }
323
474
  disposeBackgroundShells(opts) {
475
+ // 签名带 opts——core runtask 尾调 `{except: keepAlive}`(timeout/session 驻留 Monitor
476
+ // 留活名单),此前无参签名把它静默丢弃=kill-all。透传给 manager(无参=既有全灭)。
324
477
  return this._bgManager ? this._bgManager.dispose(opts) : Promise.resolve();
325
478
  }
479
+ // ─────────────────────────────── Shell: buffered exec ───────────────────────────────
480
+ //
481
+ // [1116] fg spool 化(core 1.318 修的平行盲区,当班移植):管道 stdio + 等 `close` settle 的旧形,
482
+ // 在孙进程(`nohup server &`)持有管道写端时 close 恒不发——exec 挂满 timeout → 组杀连坐孙进程。
483
+ // spool 形:子进程 stdout/stderr 直写文件 fd(进程间零管道),`close` 仍是主 settle 路,但 `exit`
484
+ // +500ms drain 兜底(close 不来也 settle);正常终态不进任何杀伤集(timeout/abort 才杀树)。
485
+ // detach adoption 走 FILE 形 handover(spool 路径+游标直接进 HostBgState——launch 同形,poll 共码,
486
+ // 单一字节流天然无 decoder 接缝)。win32 / spool 打开失败 → 回退管道旧形(core 同姿势)。
326
487
  async exec(command, options) {
327
488
  if (options?.abortSignal?.aborted)
328
489
  return { ok: false, error: new ExecutionError("aborted", "exec aborted before start") };
@@ -331,7 +492,11 @@ export class RemoteHostExecutionEnv {
331
492
  return ready;
332
493
  const cwd = this.resolve(options?.cwd ?? this.cwd);
333
494
  const timeoutMs = options?.timeout != null ? options.timeout * 1000 : this.cfg.commandTimeoutMs;
495
+ // core 1.207 design/116 detach: the Bash tool threads a `detachSignal` when the model/operator mid-flight
496
+ // ctrl+b's a running command. `ExecOpts` doesn't type it yet (core-side seam) — read it structurally.
334
497
  const detachSignal = options?.detachSignal;
498
+ // [1116] spool 形优先(POSIX;HOST_EXEC_SPOOL_ENABLED=false 逃生舱)。打开失败=回退管道旧形,永不因
499
+ // spool 基建挡 exec。
335
500
  if (process.platform !== "win32" && hostExecSpoolEnabled()) {
336
501
  const spool = this.openFgSpool();
337
502
  if (spool)
@@ -339,19 +504,37 @@ export class RemoteHostExecutionEnv {
339
504
  }
340
505
  return this.execViaPipes(command, options, cwd, timeoutMs, detachSignal);
341
506
  }
507
+ /** 管道旧形(win32 / spool 打开失败的回退;行为=1.225 及以前的 exec 原样)。 */
342
508
  execViaPipes(command, options, cwd, timeoutMs, detachSignal) {
343
509
  return new Promise((resolve) => {
510
+ // D2: bounded rolling-TAIL (core's RollingTailBuffer) — a model-driven `yes`/`cat big.bin` can't OOM the
511
+ // durable worker; keep only the last 8MB (tail = errors+exit, what the model needs) + a dropped-byte marker. O(maxBytes).
344
512
  const outBuf = new RollingTailBuffer();
345
513
  const errBuf = new RollingTailBuffer();
514
+ // design/116 detach TEE (mirrors the E2B tee-from-start): when the exec is detach-capable,
515
+ // tee every chunk from process START into the pair of drain buffers a later adoption hands over as-is. The
516
+ // per-stream StringDecoder inside BgMemStream then spans the whole stream — fg segment AND post-detach segment
517
+ // decode through ONE stateful decoder, so a multibyte codepoint straddling the detach instant survives (the
518
+ // old fg-tail→seed handoff decoded the halves separately → U+FFFD at the seam). Cost: one extra bounded
519
+ // (drop-oldest, HOST_BG_MEM_CAP) buffer pair for the exec's lifetime, only when a detachSignal is present.
346
520
  const tee = detachSignal ? { out: seedMemStream(""), err: seedMemStream("") } : undefined;
347
521
  let settled = false;
522
+ // D5 pipe-destroy grace: armed inside kill() (abort/timeout); destroys the stdout/stderr pipes if `close`
523
+ // never fires (a re-parented grandchild holding the FD). Cleared in finish() — including the real `close`
524
+ // path, which routes through finish() — so the timer never leaks. Does NOT change promise settle (the
525
+ // abort/timeout handlers already finish() synchronously below).
348
526
  let forceSettleTimer;
527
+ // Windows S1/S3 (design D1/D3): shell + group options are platform-resolved (POSIX byte-identical:
528
+ // /bin/sh + detached:true so kill(-pid) reaps the whole group; win32: Git Bash + windowsHide, kill
529
+ // side uses the taskkill tree instead of a group).
349
530
  const sh = hostShell();
350
531
  const child = spawn(sh.shell, [...sh.args, command], {
351
532
  cwd,
352
533
  env: this.mergeEnv(options?.env),
353
534
  ...spawnGroupOptions(),
354
535
  });
536
+ // core 1.321 cut-kill registry(三轮复审存量缺口):把杀柄交给上层(Bash 工具 cut 时强制终杀);
537
+ // 柄内 still-running 守卫(exitCode/signalCode)防对已亡组的 pgid 复用误杀(core 参考实现同语义)。
355
538
  if (options?.onSpawn && child.pid != null) {
356
539
  const spawnedPid = child.pid;
357
540
  try {
@@ -365,18 +548,23 @@ export class RemoteHostExecutionEnv {
365
548
  try {
366
549
  child.kill("SIGKILL");
367
550
  }
368
- catch { }
551
+ catch { /* noop */ }
369
552
  }
370
553
  } });
371
554
  }
372
- catch { }
555
+ catch { /* registry 回调失败不挡 exec(core 同姿势吞) */ }
373
556
  }
374
557
  const finish = (r) => {
375
558
  if (settled)
376
559
  return;
377
560
  settled = true;
378
561
  clearTimeout(timer);
562
+ // NOTE: the D5 grace timer is cleared in the `close` handler (pipes closed naturally), NOT here — an
563
+ // abort/timeout settle reaches finish() BEFORE `close`, so clearing here would cancel the grace before
564
+ // it can free a leaked FD (the exact case it exists for). On abort/timeout it fires after the grace.
379
565
  options?.abortSignal?.removeEventListener("abort", onAbort);
566
+ // A foreground settle must also release the never-fired detach listener — the hub's signal
567
+ // can outlive this exec (bounded 256-entry hub), and the listener closes over the child + tee buffers.
380
568
  removeDetachListener?.();
381
569
  resolve(r);
382
570
  };
@@ -384,14 +572,17 @@ export class RemoteHostExecutionEnv {
384
572
  const kill = () => {
385
573
  try {
386
574
  if (child.pid != null)
387
- killTreeHard(child.pid);
575
+ killTreeHard(child.pid); // POSIX: kill(-pid) whole group (byte-identical); win32: taskkill /F /T
388
576
  }
389
577
  catch {
390
578
  try {
391
579
  child.kill("SIGKILL");
392
580
  }
393
- catch { }
581
+ catch { /* noop */ }
394
582
  }
583
+ // D5: if the kill leaves a re-parented grandchild holding the pipes, `close` may never come — free the
584
+ // stdout/stderr FDs after a short grace. Idempotent (armPipeDestroyGrace is a no-op if already settled →
585
+ // we only ever arm one; reassigning is harmless since kill is called once per abort/timeout).
395
586
  forceSettleTimer ??= armPipeDestroyGrace(child);
396
587
  };
397
588
  const timer = setTimeout(() => {
@@ -404,38 +595,56 @@ export class RemoteHostExecutionEnv {
404
595
  };
405
596
  if (options?.abortSignal)
406
597
  options.abortSignal.addEventListener("abort", onAbort, { once: true });
598
+ // core 1.207 design/116 detach (clay 排, 1.81): mid-flight ctrl+b — ADOPT the running child as a background
599
+ // shell instead of killing it. On adoption the exec settles EARLY with `detached:{shellId}` (+ the output
600
+ // captured so far); ownership (output capture → the bg files, close handling, the BG-timeout wall, kill)
601
+ // moves to the bgManager entry. Fired after `settled` = too late (no-op). Refused over the live-shell limit
602
+ // (or on a pid-less/failed adopt) → the exec keeps running in the foreground, settling normally on close.
407
603
  const onDetach = () => {
408
604
  if (settled)
409
605
  return;
410
606
  const out = outBuf.result();
411
607
  const err = errBuf.result();
608
+ // 🔴 SYNCHRONOUS adopt (double-review): no `await` between the tee handover and the pipe-listener attach,
609
+ // so no data chunk is lost and no exec close/timeout can settle mid-adoption. The exec's ORIGINAL stdout/
610
+ // stderr listeners are removed FIRST so post-detach output flows only to the adopted memory buffers (no
611
+ // double-count into the now-orphaned outBuf) and `close` won't re-drive them. The adopted spool is the
612
+ // tee-from-start pair (already holds every chunk since process start through ONE stateful decoder — review
613
+ // #3); the truncation marker rides on the exec's OWN result string only (review LOW).
412
614
  child.stdout?.off("data", onExecOut);
413
615
  child.stderr?.off("data", onExecErr);
414
616
  const shellId = this.adoptRunningExec(child, tee.out, tee.err);
415
617
  const seedOut = markTruncated(out.text, out.droppedBytes);
416
618
  const seedErr = markTruncated(err.text, err.droppedBytes);
417
619
  if (shellId === undefined) {
620
+ // Refused (limit) or no pid → re-attach the exec listeners; the command keeps running in the foreground
621
+ // and settles normally on close (NEVER a dropped process).
418
622
  child.stdout?.on("data", onExecOut);
419
623
  child.stderr?.on("data", onExecErr);
420
624
  return;
421
625
  }
422
- clearTimeout(timer);
626
+ clearTimeout(timer); // the adopted process is the bgManager's now (its own BG-timeout wall applies)
423
627
  if (forceSettleTimer)
424
628
  clearTimeout(forceSettleTimer);
425
629
  options?.abortSignal?.removeEventListener("abort", onAbort);
426
630
  settled = true;
631
+ // cut-kill registry 注销(所有权已移交 bgManager——cut 不该再杀后台 shell;core 参考实现同位)。
427
632
  try {
428
633
  options?.onDetachAdopted?.();
429
634
  }
430
- catch { }
635
+ catch { /* noop */ }
431
636
  resolve(ok({ stdout: seedOut, stderr: seedErr, exitCode: 0, detached: { shellId } }));
432
637
  };
638
+ // Named (not inline) so onDetach can `off` them when it hands the pipes to the adopted background shell.
433
639
  const onExecOut = (d) => { outBuf.push(d); if (tee)
434
640
  feedMemStream(tee.out, d, HOST_BG_MEM_CAP); options?.onStdout?.(d.toString()); };
435
641
  const onExecErr = (d) => { errBuf.push(d); if (tee)
436
642
  feedMemStream(tee.err, d, HOST_BG_MEM_CAP); options?.onStderr?.(d.toString()); };
437
643
  child.stdout?.on("data", onExecOut);
438
644
  child.stderr?.on("data", onExecErr);
645
+ // 🔴 AFTER the exec listeners exist (TDZ): core's ToolDetachHub uses create-then-abort, so a detach that
646
+ // landed BEFORE this exec started arrives as an ALREADY-aborted signal → onDetach runs synchronously right
647
+ // here and must be able to `off`/re-`on` onExecOut/onExecErr. Same tick either way — no event can interleave.
439
648
  if (detachSignal) {
440
649
  if (detachSignal.aborted)
441
650
  onDetach();
@@ -445,11 +654,16 @@ export class RemoteHostExecutionEnv {
445
654
  }
446
655
  }
447
656
  child.on("error", (e) => {
657
+ // Failure to even spawn the shell (ENOENT etc.) — never the command's own non-zero exit.
448
658
  finish({ ok: false, error: new ExecutionError("spawn_error", `host spawn failed: ${e.message}`, e) });
449
659
  });
450
660
  child.on("close", (code, signal) => {
661
+ // `close` fired ⇒ the pipes closed naturally → clear the D5 grace (it only needs to force-destroy FDs when
662
+ // `close` NEVER comes after a kill — the re-parented-grandchild leak). Clearing here, not in finish().
451
663
  if (forceSettleTimer)
452
664
  clearTimeout(forceSettleTimer);
665
+ // A non-zero exit is a NORMAL value, not an error (ExecutionEnv contract). A kill-by-signal that
666
+ // wasn't a timeout/abort still resolves with the conventional 128+signo exit code.
453
667
  const exitCode = code ?? (signal ? 128 + (signalNumber(signal) ?? 0) : 1);
454
668
  const out = outBuf.result();
455
669
  const err = errBuf.result();
@@ -457,6 +671,8 @@ export class RemoteHostExecutionEnv {
457
671
  });
458
672
  });
459
673
  }
674
+ /** [1116] fg spool pair:tmpdir 私有根下专属子目录(五轮复审迁址,workspace=命令可写区不可放)+ "ax"
675
+ * 独占建(core openSpoolPair 同形)。失败=undefined(调用方回退管道形)。 */
460
676
  openFgSpool() {
461
677
  try {
462
678
  const dir = path.join(this.ensureSpoolRoot(), "fg-" + randomBytes(8).toString("hex"));
@@ -472,16 +688,17 @@ export class RemoteHostExecutionEnv {
472
688
  try {
473
689
  closeSync(fdOut);
474
690
  }
475
- catch { }
691
+ catch { /* noop */ }
476
692
  throw e;
477
693
  }
478
- this.liveFgSpools.add(dir);
694
+ this.liveFgSpools.add(dir); // 见 liveFgSpools 旁注:登记后 destroy() 才不会把它脚下的根删掉
479
695
  return { dir, outPath, errPath, fds: [fdOut, fdErr] };
480
696
  }
481
697
  catch {
482
698
  return undefined;
483
699
  }
484
700
  }
701
+ /** [1116] spool 形 fg exec(core node-execution-env 1.318-1.321 机制移植;坑图=core CHANGELOG 同段)。 */
485
702
  execViaSpool(command, options, cwd, timeoutMs, detachSignal, spool) {
486
703
  const EXIT_SETTLE_DRAIN_MS = 500;
487
704
  const PUMP_MS = 200;
@@ -492,10 +709,14 @@ export class RemoteHostExecutionEnv {
492
709
  const outBuf = new RollingTailBuffer();
493
710
  const errBuf = new RollingTailBuffer();
494
711
  let settled = false;
495
- let handedOver = false;
712
+ let handedOver = false; // detach adoption 拿走了 spool 文件(bg poll 续读)——settle 清理不删文件
496
713
  let pump;
497
714
  let drainTimer;
498
715
  const cursor = { out: 0, err: 0 };
716
+ // F2(二轮复审):从命名 spool **文件**移除的累计字节(仅轮转/硬帽 truncate;skipAhead 不算,
717
+ // 字节还在文件里)——与
718
+ // RollingTailBuffer.droppedBytes 独立(后者只反映内存尾窗的 head-evict,轮转 truncate 后尾窗仍满
719
+ // =0 dropped,整代对文件消失却不计)。handover 从 0 读现存文件,这个数就是 bg 看不到的历史。
499
720
  const spoolRemoved = { out: 0, err: 0 };
500
721
  const lanes = [
501
722
  { path: spool.outPath, key: "out", buf: outBuf, cb: options?.onStdout },
@@ -531,25 +752,33 @@ export class RemoteHostExecutionEnv {
531
752
  return;
532
753
  const skipped = behind - keep;
533
754
  lane.buf.recordSkippedBytes(skipped);
755
+ // 注意:skip 只推游标,字节仍留在命名文件里(handover 从 0 读**看得到**)——不计 spoolRemoved;
756
+ // 它们随后被 truncate 抹掉时按 cursor 整段计入(三轮复审:此处预计会双计)。
534
757
  cursor[lane.key] += skipped;
535
758
  }
536
- catch { }
759
+ catch { /* noop */ }
537
760
  };
538
761
  const reclaim = (lane) => {
762
+ // 轮转回收(core 坑图「配额绕过」形):游标推进到 ROTATE 以上且文件读平 → truncate 归零;文件冲破
763
+ // 硬帽(泵落后)→ 记 skipped 后强制归零(磁盘上限=诚实丢最旧)。
539
764
  if (cursor[lane.key] < ROTATE_BYTES)
540
765
  return;
541
766
  try {
542
767
  const size = statSync(lane.path).size;
543
768
  if (size === cursor[lane.key] || size >= HARD_CAP_BYTES) {
769
+ // F2 竞窗补账(三轮复审收紧):truncate 前复采 size2(stat→truncate 间 "ax" append 形可能又写)
770
+ // → 记账全部挪到 truncateSync **成功之后**(throw=文件一字未动=不记 spoolRemoved,下拍重试);
771
+ // 实际截除=size2 整段(cursor 已 pump 部分 + 未 pump 部分)。size2 采样与 truncate 之间的极窄
772
+ // append 窗仍在(活跃写者下 truncate 语义固有;量级=两条相邻 syscall 间),接受为残余误差。
544
773
  const size2 = statSync(lane.path).size;
545
774
  truncateSync(lane.path, 0);
546
775
  if (size2 > cursor[lane.key])
547
- lane.buf.recordSkippedBytes(size2 - cursor[lane.key]);
548
- spoolRemoved[lane.key] += size2;
776
+ lane.buf.recordSkippedBytes(size2 - cursor[lane.key]); // 未 pump 段对内存尾窗=跳过
777
+ spoolRemoved[lane.key] += size2; // 硬帽/轮转同式:文件真正归零的字节数
549
778
  cursor[lane.key] = 0;
550
779
  }
551
780
  }
552
- catch { }
781
+ catch { /* noop */ }
553
782
  };
554
783
  const feed = (lane, inc) => {
555
784
  lane.buf.push(inc);
@@ -557,7 +786,7 @@ export class RemoteHostExecutionEnv {
557
786
  try {
558
787
  lane.cb?.(inc.toString("utf8"));
559
788
  }
560
- catch { }
789
+ catch { /* callback 失败不挡 settle(host 旧形同容忍) */ }
561
790
  }
562
791
  };
563
792
  const pumpOnce = () => {
@@ -570,6 +799,7 @@ export class RemoteHostExecutionEnv {
570
799
  }
571
800
  };
572
801
  const drainFinal = () => {
802
+ // finalization 真尾部(core 坑图:快突发不再拿头丢尾)——先跳到只剩 FINAL_TAIL 窗,再最多 4 轮读平。
573
803
  for (const lane of lanes) {
574
804
  skipAhead(lane, FINAL_TAIL_BYTES);
575
805
  for (let i = 0; i < 4; i++) {
@@ -589,21 +819,23 @@ export class RemoteHostExecutionEnv {
589
819
  clearTimeout(drainTimer);
590
820
  drainTimer = undefined;
591
821
  }
592
- this.liveFgSpools.delete(spool.dir);
822
+ this.liveFgSpools.delete(spool.dir); // 注销:本 exec 已 settle,它不再需要这个根活着
593
823
  if (!handedOver) {
824
+ // RB-26 同型记档(core 官方裁决,发版复审 F3 对拆):幸存孙进程持继承 fd 对 unlinked inode 的
825
+ // 后续写=脱离进程自身的增长,接受不管(命名文件形=CC 同暴露且路径可见更差;进程退出即回收)。
594
826
  for (const lane of lanes) {
595
827
  try {
596
828
  unlinkSync(lane.path);
597
829
  }
598
- catch { }
830
+ catch { /* noop */ }
599
831
  }
600
832
  try {
601
833
  rmdirSync(spool.dir);
602
834
  }
603
- catch { }
835
+ catch { /* noop */ }
604
836
  }
605
837
  };
606
- let removeDetachListener;
838
+ let removeDetachListener; // F5(二轮复审):先声明——onAbort→finish 会读它,TDZ 会抛 ReferenceError
607
839
  const finish = (r) => {
608
840
  if (settled)
609
841
  return;
@@ -619,7 +851,7 @@ export class RemoteHostExecutionEnv {
619
851
  try {
620
852
  closeSync(fd);
621
853
  }
622
- catch { }
854
+ catch { /* noop */ }
623
855
  }
624
856
  };
625
857
  const sh = hostShell();
@@ -629,15 +861,18 @@ export class RemoteHostExecutionEnv {
629
861
  cwd,
630
862
  env: this.mergeEnv(options?.env),
631
863
  ...spawnGroupOptions(),
632
- stdio: ["ignore", spool.fds[0], spool.fds[1]],
864
+ stdio: ["ignore", spool.fds[0], spool.fds[1]], // 子进程直写文件——进程间零管道([1116] 病理根除点)
633
865
  });
634
866
  }
635
867
  catch (e) {
868
+ // 同步 spawn throw(如 cwd=文件的 ENOTDIR)——与管道旧形字节级对齐:executor 内 re-throw=Promise
869
+ // reject(消费方契约:core pruneWorktrees 等 await 抛才算失败;吞成 Result err 会让 reap 静默)。
636
870
  closeSpoolFds();
637
871
  cleanupSpool();
638
872
  throw e;
639
873
  }
640
- closeSpoolFds();
874
+ closeSpoolFds(); // 父进程的 fd 副本立即关(文件 size/EOF 只反映子进程写;launch FILE 形同姿势)
875
+ // core 1.321 cut-kill registry(三轮复审存量缺口):同 pipe 径——杀柄带 still-running 守卫。
641
876
  if (options?.onSpawn && child.pid != null) {
642
877
  const spawnedPid = child.pid;
643
878
  try {
@@ -651,12 +886,16 @@ export class RemoteHostExecutionEnv {
651
886
  try {
652
887
  child.kill("SIGKILL");
653
888
  }
654
- catch { }
889
+ catch { /* noop */ }
655
890
  }
656
891
  } });
657
892
  }
658
- catch { }
893
+ catch { /* registry 回调失败不挡 exec */ }
659
894
  }
895
+ // 七轮复审:'error' 监听必须在任何可提前 return 的路径(下方 F5 already-aborted 查)之前装——异步
896
+ // spawn 失败(如 cwd 不存在)以 ChildProcess 'error' 事件发出,无监听=uncaught 直接打死 worker 进程
897
+ // (plain-node 最小形实证 crash;vitest/tsx 宿主会吸收,故该面无法用测试钉住,靠此排序保证)。
898
+ // settle 后到达的 'error' 经 finish 的 settled 门自然吞掉。
660
899
  child.on("error", (e) => {
661
900
  finish({ ok: false, error: new ExecutionError("spawn_error", `host spawn failed: ${e.message}`) });
662
901
  });
@@ -666,10 +905,14 @@ export class RemoteHostExecutionEnv {
666
905
  const e = errBuf.result();
667
906
  return { stdout: markTruncated(o.text, o.droppedBytes), stderr: markTruncated(e.text, e.droppedBytes) };
668
907
  };
908
+ // F4(发版复审):exit 一到即 latch——命令已 completed,此后 timeout/abort 绝不再杀(否则 close
909
+ // 延迟/缺席的 500ms drain 窗内,一个 timeout 会把幸存进程组连坐——恰是本修要消的杀伤面),detach
910
+ // 也不再领养(completed 命令不是后台工作)。只剩 drain timer 与 close 竞争 settle。
669
911
  let exited = false;
670
912
  const timer = setTimeout(() => {
671
913
  if (exited)
672
- return;
914
+ return; // F4:completed 命令不进杀伤集(drain settle 即将给出正常结果)
915
+ // timeout 仍杀树(RUNNING 命令超时=合法杀伤面;[1116] 修的是 completed 命令被 close 不来拖进杀伤集)
673
916
  try {
674
917
  if (child.pid != null)
675
918
  killTreeHard(child.pid);
@@ -678,14 +921,14 @@ export class RemoteHostExecutionEnv {
678
921
  try {
679
922
  child.kill("SIGKILL");
680
923
  }
681
- catch { }
924
+ catch { /* noop */ }
682
925
  }
683
926
  const p = partial();
684
927
  finish({ ok: false, error: new ExecutionError("timeout", `host exec exceeded ${timeoutMs}ms; partial stdout: ${p.stdout.slice(-2048)}`) });
685
928
  }, timeoutMs);
686
929
  const onAbort = () => {
687
930
  if (exited)
688
- return;
931
+ return; // F4 同上:已 completed 的 abort=让 drain settle 正常返回,不杀幸存组
689
932
  try {
690
933
  if (child.pid != null)
691
934
  killTreeHard(child.pid);
@@ -694,31 +937,43 @@ export class RemoteHostExecutionEnv {
694
937
  try {
695
938
  child.kill("SIGKILL");
696
939
  }
697
- catch { }
940
+ catch { /* noop */ }
698
941
  }
699
942
  finish({ ok: false, error: new ExecutionError("aborted", "host exec aborted") });
700
943
  };
944
+ // F5(发版复审):AbortSignal 不重放 shellReady/workspace 建目录期间已发生的 abort——注册前必查。
701
945
  if (options?.abortSignal?.aborted) {
702
946
  onAbort();
703
947
  return;
704
948
  }
705
949
  if (options?.abortSignal)
706
950
  options.abortSignal.addEventListener("abort", onAbort, { once: true });
951
+ // F1(发版复审):泵先装再挂 detach——already-aborted 的 detachSignal 会同步跑 onDetach→finish,
952
+ // 若泵在其后才装,cleanupSpool 清不到=幽灵泵继续 truncate 已移交的文件+常驻泄漏。
707
953
  pump = setInterval(pumpOnce, PUMP_MS);
708
954
  pump.unref?.();
709
955
  const onDetach = () => {
710
956
  if (settled || exited)
711
- return;
712
- pumpOnce();
957
+ return; // F4:completed 命令不领养(它不是后台工作;drain settle 在途)
958
+ pumpOnce(); // fg 段读平(exec 结果里的 seed 完整)
959
+ // handover 游标=0(消费方契约,旧 mem 形同语义):core 的 Bash 工具在 detach 时只给模型
960
+ // 「moved to background」marker、丢弃 exec 结果的 stdout——完整历史必须活在 bg spool 里由
961
+ // BashOutput 给。spool FILE 形从 0 重读同一文件=fg+post 单一字节流零接缝零 U+FFFD(mem-tee
962
+ // 为此专门修过的病结构性不存在);已被轮转 truncate 的头部(>8MB)在 exec 结果里已 markTruncated。
963
+ // F2(发版复审):fg 期发生过轮转/skip 的丢失字节随 handover 记账(bg 首拍 poll 并入
964
+ // bytesDroppedBeforeCursor → BashOutput 的 truncated 面诚实),从 0 读只覆盖现存代。
965
+ // F2(二轮复审):handover 丢失=从命名 spool 文件真正移除的字节(spoolRemoved),不是 RollingTail
966
+ // 尾窗的 head-evict 计数——后者在轮转 truncate 后仍报 0(尾窗满),会漏计整代。
713
967
  const fgDropped = spoolRemoved.out + spoolRemoved.err;
714
968
  const shellId = this.adoptRunningExecSpool(child, spool, { out: 0, err: 0 }, fgDropped);
715
969
  if (shellId === undefined)
716
- return;
717
- handedOver = true;
970
+ return; // 拒收(limit/无 pid)→ 继续 fg,泵照跑,close 正常 settle
971
+ handedOver = true; // spool 文件归 bg(dispose 面按 FILE 形清理)
972
+ // cut-kill registry 注销(所有权已移交 bgManager;pipe 径同位)。
718
973
  try {
719
974
  options?.onDetachAdopted?.();
720
975
  }
721
- catch { }
976
+ catch { /* noop */ }
722
977
  const o = outBuf.result();
723
978
  const e = errBuf.result();
724
979
  finish(ok({ stdout: markTruncated(o.text, o.droppedBytes), stderr: markTruncated(e.text, e.droppedBytes), exitCode: 0, detached: { shellId } }));
@@ -740,6 +995,7 @@ export class RemoteHostExecutionEnv {
740
995
  const exitCode = code ?? (signal ? 128 + (signalNumber(signal) ?? 0) : 1);
741
996
  finish(ok({ stdout: markTruncated(o.text, o.droppedBytes), stderr: markTruncated(e.text, e.droppedBytes), exitCode }));
742
997
  };
998
+ // ('error' 监听已在 spawn 后立即装——七轮复审:F5 提前 return 前必须有监听,防 uncaught)
743
999
  child.on("close", (code, signal) => {
744
1000
  if (drainTimer) {
745
1001
  clearTimeout(drainTimer);
@@ -748,12 +1004,18 @@ export class RemoteHostExecutionEnv {
748
1004
  settleFromTermination(code, signal);
749
1005
  });
750
1006
  child.on("exit", (code, signal) => {
1007
+ // F4(二轮复审):exit **立即** latch——命令已 completed,此后 timeout/abort/detach 全 no-op(否则
1008
+ // close 延迟/缺席时,drain 窗内的 timeout 会 killTree 连坐幸存孙进程;一轮修漏了这行赋值)。
751
1009
  exited = true;
1010
+ // [1116] 病理根除点:exit=子 shell 已终——孙进程(nohup 服务)可以继续持有 spool 文件 fd(写文件
1011
+ // 不牵连本 exec 的 settle),500ms drain 后兜底 settle;close 先来则上面清掉本 timer 直接 settle。
752
1012
  drainTimer = setTimeout(() => settleFromTermination(code, signal), EXIT_SETTLE_DRAIN_MS);
753
1013
  drainTimer.unref?.();
754
1014
  });
755
1015
  });
756
1016
  }
1017
+ /** [1116] spool 形 detach adoption:spool 路径+fg 游标直接进 HostBgState 的 FILE 面(launch 同形,poll
1018
+ * 共码);同一文件单一字节流=天然无 decoder 接缝(mem-tee 版为此专门修过的病在此结构性不存在)。 */
757
1019
  adoptRunningExecSpool(child, spool, cursors, fgRemovedBytes = 0) {
758
1020
  const pid = child.pid;
759
1021
  if (pid == null)
@@ -774,12 +1036,16 @@ export class RemoteHostExecutionEnv {
774
1036
  });
775
1037
  return adopted?.shellId;
776
1038
  }
1039
+ // ─────────────────────────────── streaming exec ───────────────────────────────
777
1040
  async *execStream(command, options) {
778
1041
  const ready = await this.shellReady();
779
1042
  if (!ready.ok)
780
1043
  throw new RemoteExecutionError("connect_failed", ready.error.message, ready.error);
781
1044
  const cwd = this.resolve(options?.cwd ?? this.cwd);
782
1045
  const timeoutMs = options?.timeout != null ? options.timeout * 1000 : undefined;
1046
+ // Liveness is opt-in here (unlike e2b's default-on idle cap). Deliberate divergence:
1047
+ // host runs on the operator's own machine — no provider-unreachability hang class — and a missing
1048
+ // readTimeoutMs means the caller's own AbortSignal is the abort path. So no default wall-clock cap.
783
1049
  const idleMs = options?.readTimeoutMs;
784
1050
  const cap = options?.maxOutputBytes;
785
1051
  const queue = [];
@@ -790,7 +1056,7 @@ export class RemoteHostExecutionEnv {
790
1056
  let lastChunk = Date.now();
791
1057
  let wake;
792
1058
  const signalReady = () => { const w = wake; wake = undefined; w?.(); };
793
- const sh = hostShell();
1059
+ const sh = hostShell(); // Windows S1/S3: platform-resolved shell + group options (POSIX byte-identical)
794
1060
  const child = spawn(sh.shell, [...sh.args, command], { cwd, env: this.mergeEnv(options?.env), ...spawnGroupOptions() });
795
1061
  let pipeDestroyTimer;
796
1062
  const kill = () => {
@@ -802,8 +1068,12 @@ export class RemoteHostExecutionEnv {
802
1068
  try {
803
1069
  child.kill("SIGKILL");
804
1070
  }
805
- catch { }
1071
+ catch { /* noop */ }
806
1072
  }
1073
+ // S2b: same D5 pipe-destroy grace the buffered exec has — a kill-escaping process (POSIX
1074
+ // re-parented grandchild; win32 taskkill out-of-tree escapee) holding our pipes means `close` never
1075
+ // fires and the stream hangs on its own idle/abort path forever. Force-free the FDs after the grace so
1076
+ // the generator's close-driven finish always arrives. Idempotent (one timer; cleared on close).
807
1077
  pipeDestroyTimer ??= armPipeDestroyGrace(child);
808
1078
  };
809
1079
  const enqueue = (type, data) => {
@@ -825,7 +1095,7 @@ export class RemoteHostExecutionEnv {
825
1095
  });
826
1096
  child.on("close", (code, signal) => {
827
1097
  if (pipeDestroyTimer)
828
- clearTimeout(pipeDestroyTimer);
1098
+ clearTimeout(pipeDestroyTimer); // pipes closed naturally → the S2b grace is moot
829
1099
  if (finished)
830
1100
  return;
831
1101
  exitCode = code ?? (signal ? 128 + (signalNumber(signal) ?? 0) : 1);
@@ -846,6 +1116,7 @@ export class RemoteHostExecutionEnv {
846
1116
  else
847
1117
  options.signal.addEventListener("abort", onAbort, { once: true });
848
1118
  }
1119
+ // Optional total wall-clock cap (host: only when the caller asks).
849
1120
  const wallTimer = timeoutMs != null
850
1121
  ? setTimeout(() => {
851
1122
  if (finished)
@@ -872,6 +1143,7 @@ export class RemoteHostExecutionEnv {
872
1143
  throw failure;
873
1144
  if (finished)
874
1145
  break;
1146
+ // idle/liveness bound: no chunk within readTimeoutMs → typed timeout (a hung command).
875
1147
  const waitMs = idleMs != null ? Math.max(1, idleMs - (Date.now() - lastChunk)) : 30_000;
876
1148
  await new Promise((r) => { wake = r; setTimeout(r, waitMs); });
877
1149
  if (idleMs != null && !finished && Date.now() - lastChunk >= idleMs) {
@@ -890,12 +1162,13 @@ export class RemoteHostExecutionEnv {
890
1162
  if (!finished) {
891
1163
  finished = true;
892
1164
  kill();
893
- }
1165
+ } // consumer abandoned early → don't leak the child
894
1166
  if (wallTimer)
895
1167
  clearTimeout(wallTimer);
896
1168
  options?.signal?.removeEventListener("abort", onAbort);
897
1169
  }
898
1170
  }
1171
+ // ─────────────────────────────── FileSystem ───────────────────────────────
899
1172
  async absolutePath(p) {
900
1173
  return ok(this.resolve(p));
901
1174
  }
@@ -944,6 +1217,7 @@ export class RemoteHostExecutionEnv {
944
1217
  return ready;
945
1218
  const abs = this.resolve(p);
946
1219
  try {
1220
+ // Create parent dirs automatically (parity with the E2B adapter).
947
1221
  await fs.mkdir(path.dirname(abs), { recursive: true });
948
1222
  const data = typeof content === "string" ? content : Buffer.from(content);
949
1223
  await fs.writeFile(abs, data, { signal: abortSignal });
@@ -954,6 +1228,9 @@ export class RemoteHostExecutionEnv {
954
1228
  }
955
1229
  }
956
1230
  async appendFile(p, content, abortSignal) {
1231
+ // `lingeringBgWrite`:destroy 之后**仍在册**的后台 shell 还要把输出镜像到文件 —— 见 fsReady 旁注。
1232
+ // 只有 append 开这个口:core 的后台输出镜像唯一走 `env.appendFile(handle.outputFile, …)`,别的文件动作
1233
+ // 在 destroy 之后没有正当理由(读面留给下一轮的新 env)。
957
1234
  const ready = await this.fsReady(p, { lingeringBgWrite: true });
958
1235
  if (!ready.ok)
959
1236
  return ready;
@@ -976,7 +1253,7 @@ export class RemoteHostExecutionEnv {
976
1253
  return ready;
977
1254
  const abs = this.resolve(p);
978
1255
  try {
979
- const st = await fs.lstat(abs);
1256
+ const st = await fs.lstat(abs); // no follow (FileInfo contract)
980
1257
  return ok({
981
1258
  name: path.basename(abs),
982
1259
  path: abs,
@@ -1007,6 +1284,7 @@ export class RemoteHostExecutionEnv {
1007
1284
  mtimeMs = st.mtimeMs;
1008
1285
  }
1009
1286
  catch {
1287
+ /* a child that vanished between readdir and lstat → keep kind from Dirent, size/mtime 0 */
1010
1288
  }
1011
1289
  out.push({ name: e.name, path: child, kind: kindFromDirent(e), size, mtimeMs });
1012
1290
  }
@@ -1017,6 +1295,10 @@ export class RemoteHostExecutionEnv {
1017
1295
  }
1018
1296
  }
1019
1297
  async canonicalPath(p, abortSignal) {
1298
+ // 🔴 只读面也过 fsReady(2026-07-25 统一):`exists`/`canonicalPath`/`readLink` 此前**不查** destroyed,
1299
+ // 于是拆除之后「exists 答 true、紧接着 readTextFile 答 already destroyed」这一对自相矛盾 —— 调用方按
1300
+ // exists 的结果去读就撞一个措辞完全不同的错。同批把 adb 腿的读面也补上了门(它此前连 connected 都不查),
1301
+ // 这里是同一件事:**同族一致性**。只读探测本身无害,但"有害/无害"不该是各方法自行其是的判断。
1020
1302
  const ready = await this.fsReady(p);
1021
1303
  if (!ready.ok)
1022
1304
  return ready;
@@ -1029,6 +1311,10 @@ export class RemoteHostExecutionEnv {
1029
1311
  }
1030
1312
  }
1031
1313
  async readLink(p, abortSignal) {
1314
+ // 🔴 只读面也过 fsReady(2026-07-25 统一):`exists`/`canonicalPath`/`readLink` 此前**不查** destroyed,
1315
+ // 于是拆除之后「exists 答 true、紧接着 readTextFile 答 already destroyed」这一对自相矛盾 —— 调用方按
1316
+ // exists 的结果去读就撞一个措辞完全不同的错。同批把 adb 腿的读面也补上了门(它此前连 connected 都不查),
1317
+ // 这里是同一件事:**同族一致性**。只读探测本身无害,但"有害/无害"不该是各方法自行其是的判断。
1032
1318
  const ready = await this.fsReady(p);
1033
1319
  if (!ready.ok)
1034
1320
  return ready;
@@ -1041,6 +1327,10 @@ export class RemoteHostExecutionEnv {
1041
1327
  }
1042
1328
  }
1043
1329
  async exists(p, abortSignal) {
1330
+ // 🔴 只读面也过 fsReady(2026-07-25 统一):`exists`/`canonicalPath`/`readLink` 此前**不查** destroyed,
1331
+ // 于是拆除之后「exists 答 true、紧接着 readTextFile 答 already destroyed」这一对自相矛盾 —— 调用方按
1332
+ // exists 的结果去读就撞一个措辞完全不同的错。同批把 adb 腿的读面也补上了门(它此前连 connected 都不查),
1333
+ // 这里是同一件事:**同族一致性**。只读探测本身无害,但"有害/无害"不该是各方法自行其是的判断。
1044
1334
  const ready = await this.fsReady(p);
1045
1335
  if (!ready.ok)
1046
1336
  return ready;
@@ -1052,7 +1342,7 @@ export class RemoteHostExecutionEnv {
1052
1342
  catch (e) {
1053
1343
  const fe = this.fileError(e, abs, abortSignal);
1054
1344
  if (fe.code === "not_found")
1055
- return ok(false);
1345
+ return ok(false); // missing path is `false`, not an error (contract)
1056
1346
  return { ok: false, error: fe };
1057
1347
  }
1058
1348
  }
@@ -1070,12 +1360,13 @@ export class RemoteHostExecutionEnv {
1070
1360
  }
1071
1361
  }
1072
1362
  async remove(p, options) {
1073
- const ready = await this.fsReady(p);
1363
+ const ready = await this.fsReady(p); // mutation: fail closed after destroy() — parity with writeFile/createDir + e2b/ssh
1074
1364
  if (!ready.ok)
1075
1365
  return ready;
1076
1366
  const abs = this.resolve(p);
1077
1367
  try {
1078
1368
  if (!options?.force) {
1369
+ // Surface a missing path as not_found (fs.rm with force:false already throws ENOENT, but be explicit).
1079
1370
  await fs.lstat(abs);
1080
1371
  }
1081
1372
  await fs.rm(abs, { recursive: options?.recursive ?? false, force: options?.force ?? false });
@@ -1088,6 +1379,18 @@ export class RemoteHostExecutionEnv {
1088
1379
  return { ok: false, error: fe };
1089
1380
  }
1090
1381
  }
1382
+ /** 🔴 临时文件/目录**不放进 workspace**(2026-07-25 修)。
1383
+ * 原来两者都建在 `this.workspaceDir` 下,而 **TOC/CC 形的 workspaceDir 就是用户自己的目录**,且 `destroy()`
1384
+ * 明文永不删它 ⇒ core 给**每个**后台 bash 都调一次 `createTempFile({prefix:"bash-task-", suffix:".output"})`
1385
+ * ⇒ 用户仓库根目录按后台任务数累积 `bash-task-<hex>.output`(内容=命令输出)+ 每次 `createTempDir` 一个
1386
+ * `tmp-XXXXXX/`,**永久**留在他们的 git 树里。两条独立判据说明这是偏差不是设计:①core 自己的 TOC 参照实现
1387
+ * `NodeExecutionEnv.createTempFile` 放的是 `os.tmpdir()`;②**本适配器早就为了同一个理由把 spool 根搬去了
1388
+ * tmpdir**([1116] 五轮复审明写"不再弄脏 user-owned workspace"),只是 createTempFile/Dir 没跟上。
1389
+ * 改用本 env 自己的 tmp 根(`ensureSpoolRoot()`,已在 `os.tmpdir()` 下):
1390
+ * · 用户目录不再被写入;
1391
+ * · 镜像写仍然可用 —— `isLingeringMirrorTarget` 同批放行这个根(见那里的旁注);
1392
+ * · ephemeral 形反而更好:`destroy()` 在**有后台 shell 在册**时不删这个根,而 workspace 是会被 rm 的。
1393
+ * ⚠️ 这是 **host-only**:k8s/local-docker 的临时文件必须落在**容器内**可见的 cwd,ssh/e2b 本来就用远端系统 tmp。 */
1091
1394
  async createTempDir(prefix = "tmp-", abortSignal) {
1092
1395
  const ready = await this.fsReady(".");
1093
1396
  if (!ready.ok)
@@ -1107,23 +1410,29 @@ export class RemoteHostExecutionEnv {
1107
1410
  const suffix = options?.suffix ?? "";
1108
1411
  try {
1109
1412
  const name = `${prefix}${randomBytes(8).toString("hex")}${suffix}`;
1110
- const abs = path.join(this.ensureSpoolRoot(), name);
1413
+ const abs = path.join(this.ensureSpoolRoot(), name); // 见 createTempDir 上方旁注:不放进用户的 workspace
1111
1414
  if (options?.abortSignal?.aborted)
1112
1415
  throw Object.assign(new Error("aborted"), { name: "AbortError" });
1113
- await fs.writeFile(abs, "", { flag: "wx" });
1416
+ await fs.writeFile(abs, "", { flag: "wx" }); // wx = fail if exists (random → won't)
1114
1417
  return ok(abs);
1115
1418
  }
1116
1419
  catch (e) {
1117
1420
  return { ok: false, error: this.fileError(e, this.workspaceDir, options?.abortSignal) };
1118
1421
  }
1119
1422
  }
1423
+ // ─────────────────────────────── internals ───────────────────────────────
1424
+ /** Resolve a (possibly relative) path against {@link cwd} like the other adapters. Host paths are platform-native. */
1120
1425
  resolve(p) {
1121
1426
  return path.isAbsolute(p) ? path.normalize(p) : path.normalize(path.resolve(this.cwd, p));
1122
1427
  }
1428
+ /** Lazily create the per-task workspace exactly once (memoized; concurrent first-use shares it). */
1123
1429
  ensurePrepared() {
1124
1430
  if (this.prepared)
1125
1431
  return Promise.resolve();
1126
1432
  if (!this.preparing) {
1433
+ // Reset the memo on rejection so a later call can re-attempt after a transient mkdir
1434
+ // failure (ENOSPC / racy EACCES / rm race) — mirrors e2b's reset-on-failed-provision.
1435
+ // Re-throw (don't swallow) so shellReady/fsReady/connect still surface the original error.
1127
1436
  this.preparing = fs
1128
1437
  .mkdir(this.workspaceDir, { recursive: true })
1129
1438
  .then(() => {
@@ -1140,6 +1449,11 @@ export class RemoteHostExecutionEnv {
1140
1449
  if (this.destroyed)
1141
1450
  return { ok: false, error: new ExecutionError("shell_unavailable", "execution env already destroyed") };
1142
1451
  try {
1452
+ // Windows S1 (design D1): resolve the host shell ONCE (POSIX = /bin/sh, sync constant; win32 = Git Bash
1453
+ // via core getShellConfig, fail-loud when absent). Every spawn site is behind this gate, so the sync
1454
+ // `hostShell()` reads below are always post-resolution. A missing Git Bash surfaces as its own
1455
+ // shell_unavailable error with the install hint — never a silent cmd fallback, never mislabeled as a
1456
+ // workspace failure.
1143
1457
  try {
1144
1458
  await resolveHostShell();
1145
1459
  }
@@ -1153,10 +1467,48 @@ export class RemoteHostExecutionEnv {
1153
1467
  return { ok: false, error: new ExecutionError("shell_unavailable", `failed to create host workspace: ${errMsg(e)}`, asError(e)) };
1154
1468
  }
1155
1469
  }
1470
+ /**
1471
+ * @param opts.lingeringBgWrite 只由 {@link appendFile} 传 true —— 允许「destroy 之后、仍在册的后台 shell
1472
+ * 的输出镜像写」。
1473
+ *
1474
+ * 🔴 为什么需要这个口(2026-07-25,cli dogfood 报的现象追出来的真因):`destroy()` 无条件置
1475
+ * `destroyed = true`,于是文件面**全拒**;而 `pollBackground` 直通 bgManager、**没有** destroyed 门。
1476
+ * 这与本适配器自己遵守的 core 契约(1.258.2「destroy 不得收割后台进程」,上方 destroy() 里也为
1477
+ * 「dispose 前仍可轮询」的残余刻意保留了 spool 根)直接打架:我们**刻意**让后台 shell 跨 destroy 存活并
1478
+ * 继续被轮询,却拒掉它们要写的那个文件 ⇒ core 的 `appendFile` 一失败即置 `spool.mirrorFailed`,用户在一个
1479
+ * 其实成功的后台任务上看到「[!] The output file is INCOMPLETE」。已用真子进程执行验证过这个不对称。
1480
+ *
1481
+ * 放行判据刻意收紧到三条同时成立,少一条都拒:
1482
+ * ① `persistent` —— 只有 TOC/CC 形(构造时给了 `workspaceDir` = 用户自己的目录)。那一形 destroy() 明文
1483
+ * 「NEVER rm -rf it」,什么都没删,文件确实还在,core 还告诉用户「Read it any time」⇒ 报 INCOMPLETE 是谎。
1484
+ * **ephemeral 形不放行**:它的 destroy() 真的 rm -rf 了 workspace,而 `createTempFile` 恰把 outputFile
1485
+ * 建在 workspace 里 ⇒ 镜像目标已被删,拒写是**诚实**的;放行反而会让 appendFile 的 mkdir -p 把一个已拆除
1486
+ * 的工作目录**复活**成孤儿目录。
1487
+ * ② 还有后台 shell **在册** —— 判据与 destroy() 里"跳过删 spool 根"用的是同一个 `hasShells()`,即
1488
+ * 「还有人在读写这些文件」这一个事实,不是两套规则。没有 shell 在册时 `destroyed` 照旧是硬护栏。
1489
+ * ③ 只对 append 一个动作开 —— 不是把整个文件面在 destroy 之后重新开门。
1490
+ * 用 `this._bgManager?.` 而不是惰性 getter:拆除路径上不该顺手 **新建** 一个 manager。
1491
+ */
1492
+ /** destroy 后「残余镜像写」的**路径**判据 —— 与方法判据(`lingeringBgWrite`)并列,两者都满足才放行。
1493
+ *
1494
+ * 🔴 为什么需要它(深挖第一轮打在我自己头上的一条,2026-07-25):我原先的放行只看方法标志,于是 destroy 之后
1495
+ * `appendFile` 能**新建**文件、`mkdir -p` 建整棵树、甚至写 workspace **之外**的绝对路径 —— 而我在 fsReady
1496
+ * 头注里写的是「别的文件动作在 destroy 之后没有正当理由」。探针实测把这个不一致钉了出来:说是"窄",在路径
1497
+ * 维度上一点都不窄。
1498
+ *
1499
+ * 收紧到恰好贴合唯一的正当调用方(core 的后台输出镜像):
1500
+ * ① 目标必须在 **workspace 之内**(`createTempFile` 就把 outputFile 建在这儿)—— 挡住"写到工作区外面";
1501
+ * ② 目标**必须已经存在** —— 镜像写的文件是 destroy **之前**建好的,所以"新建"从来不是这条路的需求;
1502
+ * 这一条同时挡掉 `mkdir -p` 建新目录树的副作用。
1503
+ * 同步 `existsSync` 而不是 await:这是拆除路径上的判据,不该再引入一次异步让出。 */
1156
1504
  isLingeringMirrorTarget(p) {
1157
1505
  const abs = this.resolve(p);
1158
1506
  if (!existsSync(abs))
1159
- return false;
1507
+ return false; // 必须是既存文件(镜像目标在 destroy 前就已建好)
1508
+ // 🔴 必须在**同一路径空间**里比 containment:core 传进来的是 `canonicalPath()` 的产物(realpath),
1509
+ // 而 `workspaceDir` 是构造时给的原样串 —— macOS 上 `/var/...` 与 `/private/var/...` 是同一目录的两种写法,
1510
+ // 直接 `path.relative` 会把合法目标判成"在 workspace 之外"。(这条是我自己的行为面测试当场按住的:
1511
+ // 收紧判据的第一版让 `host-env-bg-mirror-after-destroy` 立刻红。)两侧都过 realpath 再比。
1160
1512
  const real = (x) => { try {
1161
1513
  return realpathSync(x);
1162
1514
  }
@@ -1167,13 +1519,23 @@ export class RemoteHostExecutionEnv {
1167
1519
  const rel = path.relative(real(root), real(abs));
1168
1520
  return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
1169
1521
  };
1522
+ // workspace **或**本 env 自己的 tmp 根:后者是 `createTempFile` 现在的落点(同批把临时文件搬出用户目录,
1523
+ // 见 createTempDir 上方旁注)。两个根都是"本 env 自有"的面,放行它们不会让残余写触及无关路径。
1524
+ // 用 `this.spoolRootDir` 而不是 `ensureSpoolRoot()`:拆除路径上不该顺手**新建**一个 tmp 根。
1170
1525
  return inside(this.workspaceDir) || (this.spoolRootDir !== undefined && inside(this.spoolRootDir));
1171
1526
  }
1172
1527
  async fsReady(p, opts) {
1173
1528
  if (this.destroyed) {
1529
+ // 🔴 判据从三条收成两条 +路径判据(2026-07-25 同批):原先还要求 `this.persistent`,理由是
1530
+ // 「ephemeral 形的 workspace 会被 rm,放行会让 mkdir -p **复活**一个已拆除的目录」。同批把临时文件搬到本 env
1531
+ // 的 tmp 根之后,那个理由**已被 `isLingeringMirrorTarget` 里的 `existsSync` 覆盖**:workspace 被删 ⇒ 目标不
1532
+ // 存在 ⇒ 自然拒;而"必须是既存文件"也让 mkdir -p 无从复活任何东西。继续要求 `persistent` 只会挡住
1533
+ // ephemeral 形**本该成功**的镜像写(目标在 tmp 根、destroy 在有 shell 在册时不删它)。
1174
1534
  const lingering = opts?.lingeringBgWrite === true && this._bgManager?.hasShells() === true && this.isLingeringMirrorTarget(p);
1175
1535
  if (!lingering)
1176
1536
  return { ok: false, error: new FileError("unknown", "execution env already destroyed", this.resolve(p)) };
1537
+ // 放行:不再走 ensurePrepared() —— destroy 已清空 preparing memo,而 persistent 形的目录本来就是用户的、
1538
+ // 必然存在(后台 shell 正在里面跑),且 appendFile 自己还会 mkdir -p 父目录。
1177
1539
  return ok(undefined);
1178
1540
  }
1179
1541
  try {
@@ -1184,6 +1546,10 @@ export class RemoteHostExecutionEnv {
1184
1546
  return { ok: false, error: new FileError("unknown", `failed to create host workspace: ${errMsg(e)}`, this.resolve(p), asError(e)) };
1185
1547
  }
1186
1548
  }
1549
+ /** Base env (process.env when inheriting) merged with the adapter's secret env, then per-command overrides win. */
1550
+ /** D1 secure default = `"scrub"`: inherit `process.env` minus secret-pattern keys (core's `scrubSecretEnv`, single
1551
+ * source of truth). `"all"` = full inherit (opt-out); a `string[]` = strict allowlist. Legacy `inheritProcessEnv`
1552
+ * maps `true`→`"all"` / `false`→`[]`. */
1187
1553
  inheritMode() {
1188
1554
  if (this.cfg.inheritEnv !== undefined)
1189
1555
  return this.cfg.inheritEnv;
@@ -1205,11 +1571,16 @@ export class RemoteHostExecutionEnv {
1205
1571
  base[k] = process.env[k];
1206
1572
  }
1207
1573
  else
1208
- base = scrubSecretEnv(process.env);
1574
+ base = scrubSecretEnv(process.env); // "scrub" — core's first-party D1 secret-env filter
1209
1575
  if (this.cfg.env)
1210
- Object.assign(base, this.cfg.env);
1576
+ Object.assign(base, this.cfg.env); // caller-supplied (control-plane secrets) verbatim
1211
1577
  if (perCommand)
1212
- Object.assign(base, perCommand);
1578
+ Object.assign(base, perCommand); // per-call wins
1579
+ // Windows S2b (design D1): win32 env keys are case-insensitive — a case-sensitive merge that
1580
+ // produced BOTH `Path` (inherited) and `PATH` (override) reaches CreateProcess as one-undefined-which.
1581
+ // Pin SHELL to the resolved Git Bash (subshell scripts read $SHELL) BEFORE the collapse (so an inherited
1582
+ // case-variant `Shell` folds into one entry), then collapse case-duplicates (later = higher-precedence
1583
+ // value, first-seen = native casing). POSIX: both are identity/no-ops — byte-identical.
1213
1584
  if (process.platform === "win32")
1214
1585
  base.SHELL = hostShell().shell;
1215
1586
  return collapseWin32EnvKeys(base);
@@ -1235,6 +1606,7 @@ export class RemoteHostExecutionEnv {
1235
1606
  return new FileError("unknown", msg, p);
1236
1607
  }
1237
1608
  }
1609
+ /** Strip anything but [A-Za-z0-9._-] from an id so it is safe in a directory name. */
1238
1610
  function sanitizeId(id) {
1239
1611
  return id.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 64) || "task";
1240
1612
  }
@@ -1251,16 +1623,42 @@ function errMsg(e) {
1251
1623
  function asError(e) {
1252
1624
  return e instanceof Error ? e : undefined;
1253
1625
  }
1626
+ /** Conventional exit code for a process killed by a signal (128 + signal number); falls back to undefined.
1627
+ * Uses Node's authoritative platform table (`os.constants.signals`) — a hand-written table missed SIGABRT/SIGPIPE
1628
+ * (`kill -ABRT` reported 137 instead of 134, impersonating SIGKILL). Callers keep the `?? 9`
1629
+ * fallback for a name the platform doesn't define, matching core's `SIGNUM[signal] ?? 9`. */
1254
1630
  function signalNumber(signal) {
1255
1631
  return os.constants.signals[signal];
1256
1632
  }
1633
+ /**
1634
+ * `ExecutionEnvFactory` for the TOC `host` backend (DUAL-MODE-DESIGN §5). Wiring this onto a deployment makes
1635
+ * its agent run DIRECTLY on the worker's machine (no container) — the local-coding-agent default. One fresh
1636
+ * env per task; the Runner owns the lifetime and calls `destroy()` (= `rm -rf` the workspace) on task end.
1637
+ *
1638
+ * Mirrors `e2bExecutionEnvFactory`: returns the env **unconnected** (lazy) and folds the per-task
1639
+ * `ctx.sessionId` into the workspace dir id so two concurrent tasks never share a working directory.
1640
+ */
1257
1641
  export function hostExecutionEnvFactory(config = {}) {
1258
1642
  return (ctx) => new RemoteHostExecutionEnv({
1259
1643
  ...config,
1260
1644
  id: config.id ?? ctx.taskId ?? ctx.sessionId,
1261
1645
  });
1262
1646
  }
1647
+ // ─────────────────────────────── remoteExec superset: docker providers ───────────────────────────────
1648
+ //
1649
+ // DUAL-MODE-DESIGN §5 defines a `remoteExec.provider` superset. `host` (above) is implemented; `local-docker`
1650
+ // is NOW IMPLEMENTED (the isolation lane) — it lives in its own module (remote-env-local-docker.ts) and is
1651
+ // re-exported here so the discriminator entry point + the historical import site stay stable. `remote-docker`
1652
+ // (a REMOTE docker daemon over TLS/SSH) is still an explicit fast-follow stub — it throws a clear "not yet
1653
+ // implemented" so selecting it fails loud at wiring time instead of silently degrading isolation. NOT faked green.
1654
+ // `local-docker` (the isolation lane): real implementation moved to remote-env-local-docker.ts. Re-export the
1655
+ // factory + config from here so existing wiring (`import { localDockerExecutionEnvFactory } from "./remote-env-host"`)
1656
+ // keeps working and the provider discriminator has one canonical entry point.
1263
1657
  export { localDockerExecutionEnvFactory, RemoteLocalDockerExecutionEnv } from "./remote-env-local-docker.js";
1658
+ /**
1659
+ * `remote-docker` factory entry point — wired but an explicit TODO (DUAL-MODE-DESIGN §5 fast-follow). Throws
1660
+ * at factory-BUILD time so selecting it before it exists fails loud rather than degrading isolation silently.
1661
+ */
1264
1662
  export function remoteDockerExecutionEnvFactory(_config) {
1265
1663
  throw new RemoteExecutionError("unsupported", "remoteExec provider 'remote-docker' is not yet implemented (DUAL-MODE-DESIGN §5 fast-follow) — use 'host', 'local-docker', or 'e2b' until it ships");
1266
1664
  }