@sema-agent/server 7.43.0 → 7.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (345) hide show
  1. package/USAGE.md +18 -0
  2. package/dist/adoption/plan.js +31 -192
  3. package/dist/adoption/quiesce.js +3 -86
  4. package/dist/adoption/runner.js +3 -140
  5. package/dist/adoption/sql.js +0 -74
  6. package/dist/adoption/wire.js +0 -80
  7. package/dist/approval-ask-machine.js +0 -75
  8. package/dist/approval-card.js +0 -323
  9. package/dist/approval-deny-reasons.js +0 -42
  10. package/dist/approval-hmac.js +0 -35
  11. package/dist/approval-reconciler.js +8 -166
  12. package/dist/approval.js +4 -67
  13. package/dist/audit.js +1 -44
  14. package/dist/auth-bridge.js +4 -47
  15. package/dist/auth-keys.js +0 -23
  16. package/dist/bake-runner/main.js +4 -65
  17. package/dist/bake-runner/protocol.js +3 -58
  18. package/dist/bake-runner/runner.js +5 -91
  19. package/dist/bench/l8/artifact.js +2 -61
  20. package/dist/bench/l8/escape.js +0 -25
  21. package/dist/bench/l8/index.js +0 -14
  22. package/dist/bench/l8/probes.js +0 -60
  23. package/dist/bench/l8/run-probes.js +2 -45
  24. package/dist/bench/s1/arms.js +14 -189
  25. package/dist/bench/s1/live-deps.js +11 -253
  26. package/dist/bench/s1/oracle.js +1 -36
  27. package/dist/bench/s1/repair-oracle-adapter.js +0 -30
  28. package/dist/bench/s1/reviewer.js +0 -34
  29. package/dist/bench/s1/row.js +0 -62
  30. package/dist/bench/s1/run-firm.js +4 -70
  31. package/dist/bench/s1/runner-ctx.js +0 -40
  32. package/dist/bench/s1/tasks.js +0 -115
  33. package/dist/boot/adoption.js +0 -20
  34. package/dist/boot/budget-tracing.js +3 -55
  35. package/dist/boot/config-center.js +43 -551
  36. package/dist/boot/coordinators.js +1 -69
  37. package/dist/boot/crash-last.js +0 -18
  38. package/dist/boot/deferred-sandbox-path-env.js +3 -119
  39. package/dist/boot/execution-env.js +5 -171
  40. package/dist/boot/governance-seams.js +0 -92
  41. package/dist/boot/leader.js +0 -69
  42. package/dist/boot/limit-sync.js +0 -2
  43. package/dist/boot/memory-boundary.js +3 -91
  44. package/dist/boot/org-memory.js +1 -25
  45. package/dist/boot/parked-revive-gate.js +0 -150
  46. package/dist/boot/permission-rules-audit.js +1 -56
  47. package/dist/boot/reapers.js +15 -273
  48. package/dist/boot/resolve-spec.js +9 -768
  49. package/dist/boot/retention-lane.js +4 -87
  50. package/dist/boot/runner-deps.js +3 -297
  51. package/dist/boot/runtime-caps.js +0 -40
  52. package/dist/boot/session-faces.js +2 -133
  53. package/dist/boot/shutdown.js +9 -83
  54. package/dist/boot/side-query-lane.js +2 -137
  55. package/dist/boot/stores.js +15 -215
  56. package/dist/boot/task-list-lane.js +0 -18
  57. package/dist/boot/webfetch-summarize-lane.js +0 -47
  58. package/dist/boot/workflow-orchestration.js +3 -99
  59. package/dist/boot-reclaim.js +0 -20
  60. package/dist/bounded-session-map.js +0 -19
  61. package/dist/brain.js +2 -139
  62. package/dist/budget.js +9 -244
  63. package/dist/capabilities/builtin-tools.js +0 -3
  64. package/dist/capabilities/center-plugins.js +6 -51
  65. package/dist/capabilities/center-prompts.js +6 -71
  66. package/dist/capabilities/code-review-council.js +7 -40
  67. package/dist/capabilities/collab-workflows.js +1 -44
  68. package/dist/capabilities/hands-lane.js +0 -65
  69. package/dist/capabilities/memory-notice.js +0 -70
  70. package/dist/capabilities/prompt.js +0 -14
  71. package/dist/capabilities/prompts/code-review.js +0 -14
  72. package/dist/capabilities/prompts/identity.js +0 -6
  73. package/dist/capabilities/prompts/team.js +0 -4
  74. package/dist/capabilities/repo-tools.js +0 -34
  75. package/dist/capabilities/sandbox-file-send.js +3 -92
  76. package/dist/capabilities/scenarios.d.ts +0 -1
  77. package/dist/capabilities/scenarios.js +18 -279
  78. package/dist/capabilities/select-environment-tool.js +0 -32
  79. package/dist/capabilities/send-user-file-tool.js +2 -66
  80. package/dist/capabilities/skills.d.ts +1 -2
  81. package/dist/capabilities/skills.js +3 -21
  82. package/dist/capabilities/team.d.ts +3 -11
  83. package/dist/capabilities/team.js +3 -51
  84. package/dist/capabilities/tool-defer.js +0 -3
  85. package/dist/config-center/apply-effective.js +15 -330
  86. package/dist/config-center/apply-ledger.js +5 -38
  87. package/dist/config-center/facade.js +0 -41
  88. package/dist/config-center/hot-keys-registry.js +0 -20
  89. package/dist/config-center/http-client.js +2 -125
  90. package/dist/config-center/mcp-revocation.js +2 -34
  91. package/dist/config-center/read-face.js +0 -59
  92. package/dist/config-center/restart-signal.js +1 -85
  93. package/dist/config-center/skills-mcp.d.ts +1 -1
  94. package/dist/config-center/skills-mcp.js +9 -76
  95. package/dist/config-center/stage-limits.js +8 -35
  96. package/dist/config-invariants.js +0 -16
  97. package/dist/config-lkg.js +0 -42
  98. package/dist/config-provider.js +3 -186
  99. package/dist/config-types.js +0 -5
  100. package/dist/config.js +107 -1145
  101. package/dist/degenerate-instrument.js +3 -67
  102. package/dist/deployment-governance.js +0 -124
  103. package/dist/digest-form.js +0 -12
  104. package/dist/elicitation.js +3 -86
  105. package/dist/env-facts.js +7 -75
  106. package/dist/fleet/fleet-bus.js +34 -507
  107. package/dist/fleet/fleet-reconciler.js +9 -149
  108. package/dist/fleet/fleet-terminal-window.js +10 -178
  109. package/dist/fleet/subagent-tail-bus.js +3 -72
  110. package/dist/fleet-client.js +10 -70
  111. package/dist/fleet-lease.js +5 -79
  112. package/dist/git-api-kind.js +0 -3
  113. package/dist/governance-ask-marks.js +2 -78
  114. package/dist/hooks/branch-transcript.js +0 -74
  115. package/dist/hooks/cc-agent-hook-prompt.js +0 -29
  116. package/dist/hooks/cc-stop-prompt.js +1 -46
  117. package/dist/hooks/hook-llm.js +1 -53
  118. package/dist/hooks/hook-runner.js +20 -414
  119. package/dist/http/active-run-conflict.js +4 -129
  120. package/dist/http/cursor-fingerprint.d.ts +5 -0
  121. package/dist/http/cursor-fingerprint.js +5 -0
  122. package/dist/http/idempotency.js +0 -37
  123. package/dist/http/principal-gate.js +3 -40
  124. package/dist/http/route-ctx.js +0 -9
  125. package/dist/http/routes/a2a-serve.js +7 -319
  126. package/dist/http/routes/admin-config-refresh.js +0 -4
  127. package/dist/http/routes/admin-drain.js +0 -10
  128. package/dist/http/routes/adoption.js +1 -29
  129. package/dist/http/routes/agents-roster.js +1 -47
  130. package/dist/http/routes/approvals-assistant.js +32 -395
  131. package/dist/http/routes/attachments.js +4 -22
  132. package/dist/http/routes/capabilities.js +3 -420
  133. package/dist/http/routes/diagnostics.js +1 -81
  134. package/dist/http/routes/fleet.js +9 -185
  135. package/dist/http/routes/images.js +14 -239
  136. package/dist/http/routes/leader.js +0 -13
  137. package/dist/http/routes/memory-bundle.js +1 -61
  138. package/dist/http/routes/memory-policy.js +9 -99
  139. package/dist/http/routes/notify-wake.js +3 -37
  140. package/dist/http/routes/observability.js +2 -19
  141. package/dist/http/routes/retention-ops.js +2 -34
  142. package/dist/http/routes/rules.js +1 -93
  143. package/dist/http/routes/runs.js +64 -873
  144. package/dist/http/routes/session-sync.js +19 -258
  145. package/dist/http/routes/sessions-list.js +17 -43
  146. package/dist/http/routes/sessions.js +34 -226
  147. package/dist/http/routes/shared-memory.js +5 -36
  148. package/dist/http/routes/side-query.js +1 -87
  149. package/dist/http/routes/tasks.js +52 -722
  150. package/dist/http/routes/trace-usage.js +32 -195
  151. package/dist/http/routes/workflows.js +19 -195
  152. package/dist/http/run-meta.js +0 -6
  153. package/dist/http/send.js +0 -32
  154. package/dist/http/server.js +92 -1597
  155. package/dist/http/sse-lifecycle.js +2 -13
  156. package/dist/http/sse-log.js +3 -48
  157. package/dist/http/tar.js +5 -21
  158. package/dist/http/verify-rounds.js +0 -5
  159. package/dist/http/wire-gate.js +0 -9
  160. package/dist/http/workspace-content.js +0 -10
  161. package/dist/images/bake-validate.js +1 -70
  162. package/dist/images/manifest.js +1 -6
  163. package/dist/index.js +0 -21
  164. package/dist/key-resolver.js +2 -17
  165. package/dist/leader/diffout.js +1 -20
  166. package/dist/leader/diffup.js +0 -47
  167. package/dist/leader/endpoint.js +2 -57
  168. package/dist/leader/fanout.js +3 -45
  169. package/dist/leader/grader-env-factory.js +3 -72
  170. package/dist/leader/leader.js +5 -156
  171. package/dist/leader/merge.js +8 -115
  172. package/dist/leader/planner.js +3 -54
  173. package/dist/leader/repair-oracle.js +1 -60
  174. package/dist/leader/repair-wire.js +2 -79
  175. package/dist/leader/wire.js +8 -307
  176. package/dist/lsp/e2b-bridge.js +4 -64
  177. package/dist/lsp/e2b-manager.js +6 -94
  178. package/dist/lsp/lsp-frames.js +0 -12
  179. package/dist/lsp/manager.js +4 -96
  180. package/dist/lsp/ws-transport.js +5 -55
  181. package/dist/lsp-evict.js +1 -15
  182. package/dist/main.js +57 -765
  183. package/dist/memory-bundle-engine.js +0 -55
  184. package/dist/memory-export.js +0 -4
  185. package/dist/memory-posture.js +1 -15
  186. package/dist/memory-scope.js +11 -148
  187. package/dist/memory-sync-client.js +2 -44
  188. package/dist/memory-sync.js +1 -80
  189. package/dist/model-select.js +3 -80
  190. package/dist/observability/cost-quota.js +1 -17
  191. package/dist/observability/cost-taxonomy.js +0 -34
  192. package/dist/observability/fail-open.js +7 -86
  193. package/dist/observability/logger.js +0 -6
  194. package/dist/observability/metrics.js +0 -94
  195. package/dist/observability/otel-exporter.js +3 -13
  196. package/dist/observability/principal-context.js +0 -9
  197. package/dist/observability/prompt-manifest.js +1 -37
  198. package/dist/observability/rate-limit.js +0 -4
  199. package/dist/observability/secret-env-scrub.js +2 -56
  200. package/dist/observability/tool-trace.js +1 -70
  201. package/dist/orchestration/hardened-vm-runner.js +4 -118
  202. package/dist/orchestration/hardened-vm-worker-runner.js +1 -26
  203. package/dist/orchestration/hardened-vm-worker.js +0 -27
  204. package/dist/orchestration/subagent-steer.js +1 -45
  205. package/dist/orchestration/workflow-agent-steer.js +1 -80
  206. package/dist/orchestration/workflow-completion-inbox.js +32 -285
  207. package/dist/orchestration/workflow-notify-journal.js +16 -259
  208. package/dist/org-memory-admission.js +3 -47
  209. package/dist/parent-watch.js +2 -48
  210. package/dist/parked-decide.js +1 -109
  211. package/dist/per-task-image.js +0 -57
  212. package/dist/plan-cache-probe.js +3 -27
  213. package/dist/plugins/adoption-log-sql.js +2 -119
  214. package/dist/plugins/approval-ask-store-memory.js +3 -38
  215. package/dist/plugins/approval-ask-store-sql.js +6 -188
  216. package/dist/plugins/approval-exemption-store.js +2 -28
  217. package/dist/plugins/background-agent-store-sql.js +4 -105
  218. package/dist/plugins/background-shell-support.js +14 -122
  219. package/dist/plugins/blob-backend.js +6 -169
  220. package/dist/plugins/breaker-state-sql.js +8 -46
  221. package/dist/plugins/caching-session-store.js +4 -106
  222. package/dist/plugins/checkpoint-store-sql.js +18 -505
  223. package/dist/plugins/e2b-orphan-reclaim.js +0 -45
  224. package/dist/plugins/file-outcome-sink.js +0 -9
  225. package/dist/plugins/file-resume-anchor-store.js +4 -43
  226. package/dist/plugins/file-run-store.js +26 -364
  227. package/dist/plugins/file-snapshot-store-sql.js +10 -181
  228. package/dist/plugins/fork-routing-session-store.js +8 -111
  229. package/dist/plugins/host-platform.js +2 -91
  230. package/dist/plugins/image-bake-store-sql.js +7 -250
  231. package/dist/plugins/image-index-sql.js +4 -123
  232. package/dist/plugins/k8s-bg-scripts.js +4 -88
  233. package/dist/plugins/k8s-exec-protocol.js +0 -41
  234. package/dist/plugins/leader-run-store-sql.js +0 -101
  235. package/dist/plugins/local-checkpoint-store.js +8 -128
  236. package/dist/plugins/local-session-store.js +34 -296
  237. package/dist/plugins/local-task-attachment-store.js +2 -16
  238. package/dist/plugins/mailbox-store-sql.js +8 -65
  239. package/dist/plugins/memory-embedder-fingerprint.js +5 -166
  240. package/dist/plugins/memory-embedder.js +4 -70
  241. package/dist/plugins/memory-engine-pg.js +6 -170
  242. package/dist/plugins/memory-engine-tidb.js +7 -155
  243. package/dist/plugins/memory-engine-vector-util.js +0 -10
  244. package/dist/plugins/memory-key-guards.js +0 -34
  245. package/dist/plugins/memory-origin-law.js +0 -187
  246. package/dist/plugins/memory-resume-anchor-store.js +0 -17
  247. package/dist/plugins/memory-run-store.js +12 -89
  248. package/dist/plugins/memory-session-policy-store.js +0 -17
  249. package/dist/plugins/memory-sync-store-pg.js +4 -49
  250. package/dist/plugins/memory-sync-store-tidb.js +3 -35
  251. package/dist/plugins/outcome-ledger-sql.js +3 -97
  252. package/dist/plugins/permission-rule-store-file.js +5 -133
  253. package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
  254. package/dist/plugins/permission-rule-store-sql.js +50 -307
  255. package/dist/plugins/pg-cost-quota.js +0 -7
  256. package/dist/plugins/pg-pool.js +0 -92
  257. package/dist/plugins/pg-rate-limiter.js +2 -13
  258. package/dist/plugins/pg-safe-json.js +4 -40
  259. package/dist/plugins/pg-session-storage.js +25 -189
  260. package/dist/plugins/posix-shell-fs.js +1 -31
  261. package/dist/plugins/remote-env-adb.js +12 -101
  262. package/dist/plugins/remote-env-e2b.js +40 -371
  263. package/dist/plugins/remote-env-file-error.js +0 -37
  264. package/dist/plugins/remote-env-host.js +63 -483
  265. package/dist/plugins/remote-env-k8s.js +32 -326
  266. package/dist/plugins/remote-env-local-docker.js +24 -157
  267. package/dist/plugins/remote-env-ssh.js +20 -128
  268. package/dist/plugins/remote-scratchpad.js +2 -32
  269. package/dist/plugins/remote-shell.js +1 -32
  270. package/dist/plugins/resume-anchor-store-sql.js +0 -11
  271. package/dist/plugins/retention-lane-store-sql.js +0 -108
  272. package/dist/plugins/retention-store-sql.js +4 -383
  273. package/dist/plugins/roster-store-sql.js +0 -55
  274. package/dist/plugins/run-store-sql.js +14 -251
  275. package/dist/plugins/s3-presign.js +2 -49
  276. package/dist/plugins/scheduler-support.js +3 -80
  277. package/dist/plugins/send-file-ledger.js +4 -53
  278. package/dist/plugins/send-user-file.js +4 -94
  279. package/dist/plugins/session-placement.js +1 -89
  280. package/dist/plugins/session-policy-store-sql.js +3 -81
  281. package/dist/plugins/session-store.js +0 -57
  282. package/dist/plugins/shared-memory-store-sql.js +4 -167
  283. package/dist/plugins/sql-driver.js +0 -17
  284. package/dist/plugins/sql-errors.js +0 -7
  285. package/dist/plugins/sql-escape.js +0 -8
  286. package/dist/plugins/sql-row-helpers.js +0 -25
  287. package/dist/plugins/store-backend.js +42 -222
  288. package/dist/plugins/store-contracts.js +2 -46
  289. package/dist/plugins/task-attachment-store.js +3 -49
  290. package/dist/plugins/task-list-store-sql.js +0 -79
  291. package/dist/plugins/tidb-cost-quota.js +1 -4
  292. package/dist/plugins/tidb-pool.js +1 -207
  293. package/dist/plugins/tidb-rate-limiter.js +3 -9
  294. package/dist/plugins/tidb-session-storage.js +4 -70
  295. package/dist/plugins/tidb-session-store.js +19 -341
  296. package/dist/plugins/tool-result-store-sql.js +4 -179
  297. package/dist/plugins/usage-window-store-sql.js +0 -8
  298. package/dist/plugins/web-search.js +10 -132
  299. package/dist/plugins/workflow-journal-store-sql.js +2 -58
  300. package/dist/plugins/workflow-run-store-sql.js +6 -91
  301. package/dist/plugins/worktree-isolation.js +6 -126
  302. package/dist/plugins/write-behind-counter.js +16 -75
  303. package/dist/principal-jwt.js +5 -60
  304. package/dist/project-memory.js +15 -146
  305. package/dist/prompts-domain-validate.js +1 -51
  306. package/dist/question.js +2 -111
  307. package/dist/resource-suspend.js +0 -18
  308. package/dist/router/route-orchestration.js +0 -77
  309. package/dist/rules-consent.d.ts +44 -5
  310. package/dist/rules-consent.js +75 -215
  311. package/dist/run-local.js +14 -381
  312. package/dist/runs.js +29 -617
  313. package/dist/runtime-caps-resolver.js +6 -132
  314. package/dist/runtime-governance.js +1 -232
  315. package/dist/sandbox-pkg-source.js +0 -37
  316. package/dist/sealed-key.js +3 -68
  317. package/dist/security.js +6 -291
  318. package/dist/session-leaf-bus.js +0 -32
  319. package/dist/session-sync-content.js +1 -69
  320. package/dist/session-sync-kernel.js +3 -60
  321. package/dist/session-sync.js +3 -66
  322. package/dist/session-titler.js +7 -41
  323. package/dist/session-watch.js +9 -73
  324. package/dist/shared-memory-scope-authorizer.js +0 -18
  325. package/dist/sighup-idle.js +1 -12
  326. package/dist/spec-fields.js +4 -143
  327. package/dist/store-live-probe.js +3 -49
  328. package/dist/task-a2a.js +1 -126
  329. package/dist/task-cwd.js +1 -103
  330. package/dist/task-mcp.js +1 -89
  331. package/dist/task-settings.js +10 -300
  332. package/dist/task-workflow.js +8 -75
  333. package/dist/tool-approval.d.ts +28 -1
  334. package/dist/tool-approval.js +70 -1430
  335. package/dist/trace/artifacts.js +5 -20
  336. package/dist/trace/engine-notice-wire.js +6 -138
  337. package/dist/trace/ledger-sink.js +7 -96
  338. package/dist/trace/project.js +4 -448
  339. package/dist/trace/redact.js +9 -77
  340. package/dist/turn-activity.js +1 -27
  341. package/dist/usage-analytics.js +4 -34
  342. package/dist/wall-clock-jump-guard.js +1 -68
  343. package/package.json +3 -3
  344. package/dist/capabilities/scenario-alias.d.ts +0 -27
  345. package/dist/capabilities/scenario-alias.js +0 -61
@@ -1,42 +1,3 @@
1
- /**
2
- * `RemoteLocalDockerExecutionEnv` — the TOC **local-docker** exec adapter (DUAL-MODE-DESIGN §5, the
3
- * fast-follow promoted from the host file's typed stub). Runs the agent's bash/file tools inside a long-lived
4
- * container ON THIS MACHINE's docker daemon, so a single-machine ("take-over-this-computer") deployment gets a
5
- * REAL OS-level isolation boundary — the in-between lane between the un-isolated `host` adapter (no container)
6
- * and the fleet `e2b`/`k8s` sandboxes (remote microVMs/pods).
7
- *
8
- * It is a PEER adapter to host / e2b / k8s / ssh / adb (NOT chained): all implement the SAME core seam
9
- * `RemoteExecutionEnv` (core src/core/remote-env.ts, design/48 §5), selected by `remoteExec.provider` —
10
- * this one is `provider:"local-docker"`.
11
- *
12
- * 🔴 **Isolation, NOT a snapshot sandbox** (DUAL-MODE-DESIGN §5 / design/61 §9):
13
- * - `capabilities = { isolation: true, suspendable: false }`. The container is a true isolation boundary
14
- * (separate filesystem/PID/mount namespaces), so the agent's writes/exec are confined to it and parallel
15
- * fan-out tasks don't collide. But v1 is **NOT suspendable** — `suspendVM`/`resumeVM`/`postResumeInit`
16
- * return typed `"unsupported"`. (`docker pause`/`docker commit` could give a coarse suspend later; we do
17
- * NOT fake it here — durable suspend is honestly N/A until that lands.)
18
- * - `destroy()` / `cleanup()` **`docker rm -f` the container** — a full teardown (the container IS the
19
- * per-task sandbox, unlike host which only `rm -rf`s a dir). Idempotent, best-effort, never throws.
20
- * - `reconnect()` is supported as a liveness re-check: if the container is still running it is an idempotent
21
- * success; if it's gone (crashed / externally `rm`'d) it FAILS (`connect_failed`) — there is no managed
22
- * snapshot to resume from, so a dead container cannot be re-attached.
23
- *
24
- * 🔴 **Domestic-images iron rule (clay):** the base `image` comes ENTIRELY from config (no hardcoded
25
- * `docker.io/...` default). A deployment points it at the sandbox-image-pool (docker.io/claybobby public pool)
26
- * or its own private registry — SWR was deprecated 2026-07-13 and the in-network Gitea registry container was
27
- * retired 2026-08 (no CN fallback is baked in). This adapter never invents a registry.
28
- *
29
- * 🔴 **argv anti-injection (council, host/ssh §4):** every `docker` invocation is a `spawn(binary, argv[])`
30
- * with NO host-side shell (`shell:false` default) — the container id, image, paths, and env values are
31
- * SEPARATE argv elements, never string-concatenated into a `sh -c` on the host. The agent's COMMAND itself is
32
- * still a shell command (it runs the container's `/bin/sh -c <command>`, exactly like the host/ssh adapters
33
- * run the operator's command in a shell) — that is the agent's intended surface, bounded by the container.
34
- *
35
- * **never-throw invariant:** every base `ExecutionEnv` FileSystem/Shell method encodes failure in its returned
36
- * `Result` (never throws) — docker missing from PATH, a command timeout, a container that won't start, all
37
- * become `Result.error`. `execStream`'s iterator MAY throw `RemoteExecutionError` mid-stream before an `exit`
38
- * chunk (the seam contract). A non-zero command exit is a NORMAL value (`{ ok:true, exitCode }`), never an error.
39
- */
40
1
  import path from "node:path";
41
2
  import os from "node:os";
42
3
  import fs from "node:fs/promises";
@@ -48,13 +9,8 @@ import { FileError, ExecutionError, RemoteExecutionError, RollingTailBuffer, mar
48
9
  import { fileErrorFromExec, classifyFsStderr } from "./remote-env-file-error.js";
49
10
  import { createPosixShellFs } from "./posix-shell-fs.js";
50
11
  const PROVIDER = "local-docker";
51
- /** Default workspace root INSIDE the container (the agent's tools resolve relative paths against it). */
52
12
  const DEFAULT_MOUNT_PATH = "/workspace";
53
- /** Per-command wall-clock when the caller passes none (a single exec bound, NOT the container lifetime).
54
- * ⚠️ 同判据三腿之一(另两条:`remote-env-host.ts` 同值 30min、`remote-env-ssh.ts` **5min**)——分歧成文
55
- * 与候裁说明写在 ssh 那处的 `commandTimeoutMs` 顶注(#176 二轮扫描 finding)。 */
56
13
  const DEFAULT_COMMAND_TIMEOUT_MS = 30 * 60_000;
57
- /** Wall-clock for a control-plane docker op (run/rm/inspect/cp) — should be fast; a hang surfaces typed. */
58
14
  const DEFAULT_CONTROL_TIMEOUT_MS = 60_000;
59
15
  const ok = (value) => ({ ok: true, value });
60
16
  const unsupported = (op) => ({
@@ -62,27 +18,18 @@ const unsupported = (op) => ({
62
18
  error: new RemoteExecutionError("unsupported", `${op} is not supported on a local-docker target (v1: a container is an isolation boundary but not a managed memory snapshot; capabilities.suspendable=false)`),
63
19
  });
64
20
  export class RemoteLocalDockerExecutionEnv {
65
- /** DUAL-MODE-DESIGN §5: a container is a real isolation boundary; v1 has no managed memory snapshot. */
66
21
  capabilities = { isolation: true, suspendable: false };
67
- /** Working directory inside the container; relative paths resolve against it (ExecutionEnv contract). */
68
22
  cwd;
69
23
  cfg;
70
24
  spawnFn;
71
- /** Stable container NAME (also our handle id). Created on connect; targeted by every docker exec/cp/rm. */
72
25
  containerName;
73
- /** Provider name put on the WorkspaceHandle ("local-docker" | "remote-docker"). Observability only. */
74
26
  providerName;
75
- /** Set once the container is up + the workspace dir created. */
76
27
  containerId;
77
28
  handle;
78
- /** Terminal once {@link destroy} ran — guards exec/fs from running against a removed container. */
79
29
  destroyed = false;
80
- /** In-flight connect, memoized so concurrent lazy first-use starts exactly one container (no double-run). */
81
30
  connecting;
82
31
  constructor(config, deps = {}) {
83
32
  if (!config.image || !config.image.trim()) {
84
- // image is required (no docker.io default) — but the constructor must not throw (never-throw posture):
85
- // record the empty image and let connect() fail closed with a typed error.
86
33
  config = { ...config, image: config.image ?? "" };
87
34
  }
88
35
  this.cfg = {
@@ -96,7 +43,7 @@ export class RemoteLocalDockerExecutionEnv {
96
43
  this.cwd = this.cfg.mountPath;
97
44
  this.spawnFn = deps.spawn ?? spawn;
98
45
  const id = config.id ?? randomBytes(6).toString("hex");
99
- const suffix = randomBytes(4).toString("hex"); // always-unique tail so a reused id never collides
46
+ const suffix = randomBytes(4).toString("hex");
100
47
  this.containerName = `sema-docker-${sanitizeId(id)}-${suffix}`;
101
48
  this.providerName = config.providerLabel?.trim() || PROVIDER;
102
49
  }
@@ -108,31 +55,27 @@ export class RemoteLocalDockerExecutionEnv {
108
55
  throw new RemoteExecutionError("connect_failed", "workspaceHandle() called before connect()");
109
56
  return this.handle;
110
57
  }
111
- // ─────────────────────────────── lifecycle ───────────────────────────────
112
58
  async connect(config) {
113
59
  if (config?.snapshotId)
114
- return unsupported("connect(snapshotId)"); // no managed snapshots
60
+ return unsupported("connect(snapshotId)");
115
61
  if (this.destroyed)
116
62
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env already destroyed") };
117
63
  if (config?.abortSignal?.aborted)
118
64
  return { ok: false, error: new RemoteExecutionError("aborted", "connect aborted") };
119
65
  if (this.containerId)
120
- return ok(this.workspaceHandle()); // idempotent: already running
66
+ return ok(this.workspaceHandle());
121
67
  if (this.connecting)
122
- return this.connecting; // concurrent first-use shares one container start
68
+ return this.connecting;
123
69
  this.connecting = this.doConnect(config);
124
70
  const r = await this.connecting;
125
71
  if (!r.ok)
126
- this.connecting = undefined; // failed start → allow a later retry
72
+ this.connecting = undefined;
127
73
  return r;
128
74
  }
129
75
  async doConnect(config) {
130
76
  if (!this.cfg.image.trim()) {
131
77
  return { ok: false, error: new RemoteExecutionError("connect_failed", "local-docker: no base image configured (the image is required — no docker.io default)") };
132
78
  }
133
- // Start a long-lived detached container that idles forever (the agent's commands are `docker exec`'d into
134
- // it). `tail -f /dev/null` is the most portable idle keep-alive (no `sleep infinity` dependency on some
135
- // busybox/alpine images). --init reaps zombie exec children. The workspace dir is created via the same run.
136
79
  const pidsLimit = this.cfg.pidsLimit ?? 512;
137
80
  const runArgs = [
138
81
  "run",
@@ -142,8 +85,6 @@ export class RemoteLocalDockerExecutionEnv {
142
85
  this.containerName,
143
86
  "-w",
144
87
  this.cfg.mountPath,
145
- // Always-safe sandbox hardening (no build breakage): block setuid privilege escalation + cap the process
146
- // count so generated code can't fork-bomb the shared host kernel. cap-drop is opt-in (it DOES break some builds).
147
88
  "--security-opt",
148
89
  "no-new-privileges",
149
90
  ...(pidsLimit > 0 ? ["--pids-limit", String(pidsLimit)] : []),
@@ -154,19 +95,17 @@ export class RemoteLocalDockerExecutionEnv {
154
95
  this.cfg.image,
155
96
  "/bin/sh",
156
97
  "-c",
157
- // Create the workspace then idle. mkdir -p is harmless if the image already has it.
158
98
  `mkdir -p ${shellQuote(this.cfg.mountPath)} && exec tail -f /dev/null`,
159
99
  ];
160
100
  const run = await this.docker(runArgs, { signal: config?.abortSignal, timeoutMs: this.cfg.controlTimeoutMs });
161
101
  if (!run.ok) {
162
- await this.forceRemove().catch(() => { }); // a half-created container must not leak on a failed start
102
+ await this.forceRemove().catch(() => { });
163
103
  return { ok: false, error: this.dockerToRemoteError(run.error, "connect_failed") };
164
104
  }
165
105
  if (run.value.exitCode !== 0) {
166
106
  await this.forceRemove().catch(() => { });
167
107
  return { ok: false, error: new RemoteExecutionError("connect_failed", `docker run failed: ${run.value.stderr.trim() || `exit ${run.value.exitCode}`}`) };
168
108
  }
169
- // Race with destroy() (the awaits above yield): a destroy that landed must win — remove + bail (no orphan).
170
109
  if (this.destroyed) {
171
110
  await this.forceRemove().catch(() => { });
172
111
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env destroyed during connect") };
@@ -174,7 +113,6 @@ export class RemoteLocalDockerExecutionEnv {
174
113
  this.containerId = run.value.stdout.trim() || this.containerName;
175
114
  this.handle = { sandboxId: this.containerName, provider: this.providerName, mountPath: this.cfg.mountPath, sessionToken: this.containerName };
176
115
  if (config?.secrets?.length) {
177
- // Secrets are injected per-command via cfg.env (resolved by the control plane), not at connect — record only.
178
116
  }
179
117
  return ok(this.handle);
180
118
  }
@@ -187,11 +125,6 @@ export class RemoteLocalDockerExecutionEnv {
187
125
  async postResumeInit() {
188
126
  return unsupported("postResumeInit");
189
127
  }
190
- /**
191
- * Liveness re-check (NOT a snapshot resume): if the container is still running → idempotent success; if it's
192
- * gone (crashed / externally removed) → `connect_failed` (no managed snapshot to re-attach to). `sessionToken`
193
- * is the container name (== our handle id); a mismatch with this env's container fails closed.
194
- */
195
128
  async reconnect(sessionToken) {
196
129
  if (this.destroyed)
197
130
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env already destroyed") };
@@ -200,39 +133,28 @@ export class RemoteLocalDockerExecutionEnv {
200
133
  }
201
134
  const insp = await this.docker(["inspect", "-f", "{{.State.Running}}", this.containerName], { timeoutMs: this.cfg.controlTimeoutMs });
202
135
  if (insp.ok && insp.value.exitCode === 0 && insp.value.stdout.trim() === "true") {
203
- // 🔴 mark CONNECTED to the live container: else ensureConnectedExec/fsReady (gate on containerId) would
204
- // re-run `docker run --name <same>` → name conflict → the failure path rm -f's the very container we just
205
- // re-attached to (destroying the running workspace). The name IS our exec/handle target.
206
136
  this.containerId = this.containerName;
207
137
  this.handle ??= { sandboxId: this.containerName, provider: this.providerName, mountPath: this.cfg.mountPath, sessionToken: this.containerName };
208
138
  return ok(this.handle);
209
139
  }
210
140
  return { ok: false, error: new RemoteExecutionError("connect_failed", `container '${this.containerName}' is not running (gone or stopped) — no managed snapshot to resume`) };
211
141
  }
212
- /** `docker rm -f` the container (full teardown). Idempotent, best-effort, never throws (cleanup contract). */
213
142
  async destroy() {
214
143
  if (this.destroyed)
215
- return; // idempotent — safe double-call (Runner end + reaper)
144
+ return;
216
145
  this.destroyed = true;
217
- // 🔴 AWAIT any racing `docker run` before reaping: a destroy that lands mid-connect must not resolve before
218
- // the container exists, else forceRemove (by name) no-ops now and the just-created container LEAKS forever
219
- // (docker has no self-timeout). Mirrors the host adapter's await-prepare-then-rm.
220
146
  const inflight = this.connecting;
221
147
  this.connecting = undefined;
222
148
  this.containerId = undefined;
223
149
  await inflight?.catch(() => { });
224
- // Remove by NAME (covers a container that started but whose id we never recorded) — `rm -f` no-ops a
225
- // never-created name; best-effort, never throws.
226
150
  await this.forceRemove().catch(() => { });
227
151
  }
228
152
  async cleanup() {
229
153
  return this.destroy();
230
154
  }
231
155
  async forceRemove() {
232
- // best-effort, short bound; swallow everything (teardown must never throw).
233
156
  await this.docker(["rm", "-f", this.containerName], { timeoutMs: this.cfg.controlTimeoutMs }).catch(() => { });
234
157
  }
235
- // ─────────────────────────────── Shell: buffered exec ───────────────────────────────
236
158
  async exec(command, options) {
237
159
  if (options?.abortSignal?.aborted)
238
160
  return { ok: false, error: new ExecutionError("aborted", "exec aborted before start") };
@@ -248,10 +170,8 @@ export class RemoteLocalDockerExecutionEnv {
248
170
  });
249
171
  if (!r.ok)
250
172
  return { ok: false, error: r.error };
251
- // A non-zero exit is a NORMAL value (ExecutionEnv contract) — `docker exec` propagates the inner exit code.
252
173
  return ok({ stdout: r.value.stdout, stderr: r.value.stderr, exitCode: r.value.exitCode });
253
174
  }
254
- // ─────────────────────────────── streaming exec ───────────────────────────────
255
175
  async *execStream(command, options) {
256
176
  const ready = await this.ensureConnectedExec();
257
177
  if (!ready.ok)
@@ -279,7 +199,7 @@ export class RemoteLocalDockerExecutionEnv {
279
199
  if (queue.length >= 512 && !paused) {
280
200
  paused = true;
281
201
  child.stdout?.pause();
282
- } // backpressure
202
+ }
283
203
  signalReady();
284
204
  };
285
205
  child.stdout?.on("data", (d) => enqueue("stdout", d.toString()));
@@ -307,7 +227,7 @@ export class RemoteLocalDockerExecutionEnv {
307
227
  try {
308
228
  child.kill("SIGKILL");
309
229
  }
310
- catch { /* noop */ }
230
+ catch { }
311
231
  }
312
232
  };
313
233
  if (options?.signal) {
@@ -319,7 +239,7 @@ export class RemoteLocalDockerExecutionEnv {
319
239
  try {
320
240
  child.kill("SIGKILL");
321
241
  }
322
- catch { /* noop */ }
242
+ catch { }
323
243
  signalReady();
324
244
  };
325
245
  if (options.signal.aborted)
@@ -337,7 +257,7 @@ export class RemoteLocalDockerExecutionEnv {
337
257
  try {
338
258
  child.kill("SIGKILL");
339
259
  }
340
- catch { /* noop */ }
260
+ catch { }
341
261
  throw new RemoteExecutionError("unknown", `execStream output exceeded maxOutputBytes=${cap}`);
342
262
  }
343
263
  }
@@ -350,7 +270,6 @@ export class RemoteLocalDockerExecutionEnv {
350
270
  if (finished)
351
271
  break;
352
272
  const waitMs = idleMs != null ? Math.max(1, idleMs - (Date.now() - lastChunk)) : 30_000;
353
- // [4947] A-002.11 sibling: clear the losing timer — wake() firing first used to leave it armed.
354
273
  let waitTimer;
355
274
  try {
356
275
  await new Promise((r) => { wake = r; waitTimer = setTimeout(r, waitMs); });
@@ -364,7 +283,7 @@ export class RemoteLocalDockerExecutionEnv {
364
283
  try {
365
284
  child.kill("SIGKILL");
366
285
  }
367
- catch { /* noop */ }
286
+ catch { }
368
287
  throw new RemoteExecutionError("timeout", `execStream idle > ${idleMs}ms (command hang)`);
369
288
  }
370
289
  }
@@ -378,7 +297,6 @@ export class RemoteLocalDockerExecutionEnv {
378
297
  teardown();
379
298
  }
380
299
  }
381
- // ─────────────────────────────── FileSystem ───────────────────────────────
382
300
  async absolutePath(p) {
383
301
  return ok(this.resolve(p));
384
302
  }
@@ -390,13 +308,12 @@ export class RemoteLocalDockerExecutionEnv {
390
308
  if (!ready.ok)
391
309
  return ready;
392
310
  const abs = this.resolve(p);
393
- // `docker exec ... cat <file>` streams raw bytes on stdout (binary-safe); stderr carries cat's error.
394
311
  const r = await this.docker(this.execRaw(`cat ${shellQuote(abs)}`), { signal: abortSignal, timeoutMs: this.cfg.commandTimeoutMs, binary: true });
395
312
  if (!r.ok)
396
313
  return { ok: false, error: fileErrorFromExec(r.error, abs) };
397
314
  if (r.value.exitCode !== 0)
398
315
  return { ok: false, error: classifyFsStderr(r.value.stderr, abs) };
399
- return ok(r.value.stdoutBytes ?? new Uint8Array()); // binary:true always sets stdoutBytes (empty file → empty); ?? satisfies the now-optional type
316
+ return ok(r.value.stdoutBytes ?? new Uint8Array());
400
317
  }
401
318
  async readTextFile(p, abortSignal) {
402
319
  const r = await this.readBinaryFile(p, abortSignal);
@@ -418,7 +335,6 @@ export class RemoteLocalDockerExecutionEnv {
418
335
  if (!ready.ok)
419
336
  return ready;
420
337
  const abs = this.resolve(p);
421
- // Create parent dirs inside the container (parity with the host/e2b adapters' auto-create).
422
338
  const dir = path.posix.dirname(abs);
423
339
  if (dir && dir !== "." && dir !== "/") {
424
340
  const mk = await this.docker(this.execRaw(`mkdir -p ${shellQuote(dir)}`), { signal: abortSignal, timeoutMs: this.cfg.commandTimeoutMs });
@@ -427,8 +343,6 @@ export class RemoteLocalDockerExecutionEnv {
427
343
  if (mk.value.exitCode !== 0)
428
344
  return { ok: false, error: classifyFsStderr(mk.value.stderr, dir) };
429
345
  }
430
- // Stage to a host temp file then `docker cp` (binary-safe; avoids shell-escaping the payload — same
431
- // discipline as the ADB adapter's `adb push`). `docker cp <src> <name>:<abs>` writes into the container.
432
346
  let local;
433
347
  try {
434
348
  local = path.join(os.tmpdir(), `docker-cp-${Date.now()}-${randomBytes(6).toString("hex")}`);
@@ -464,14 +378,9 @@ export class RemoteLocalDockerExecutionEnv {
464
378
  merged.set(add, base.length);
465
379
  return this.writeFile(abs, merged, abortSignal);
466
380
  }
467
- // ── design/158 B6:POSIX shell-out 文件面全部委托共享基座(posix-shell-fs.ts)─────────────────────
468
- // 六裁决与 readLink・lstat 契约见基座头注。本腿此前的 fsErrorFromStderr 无 not_found 缺省(缺文件报
469
- // unknown),且 `|not found` 宽包含会把「命令缺失」误判成「文件不存在」—— 两处都被基座正形纠正。
470
381
  posixFsInst;
471
382
  get posixFs() {
472
383
  return (this.posixFsInst ??= createPosixShellFs({
473
- // 承 fsReady 契约(其余三腿走公有 exec、守卫天然在;本腿直用私有 docker,必须自带):
474
- // destroyed 守卫 + 懒连接,fail closed 不抛 —— 消息保留 "destroyed" 字样(测试钉的就是它)。
475
384
  run: async (cmd, abortSignal) => {
476
385
  if (this.destroyed)
477
386
  return { ok: false, error: new ExecutionError("shell_unavailable", "execution env already destroyed") };
@@ -513,63 +422,43 @@ export class RemoteLocalDockerExecutionEnv {
513
422
  async createTempFile(options) {
514
423
  return this.posixFs.createTempFile(options);
515
424
  }
516
- // ─────────────────────────────── internals ───────────────────────────────
517
- /** Prepend `-H <dockerHost>` to a docker argv when configured (per-invocation, not the process env). */
518
425
  withHostFlag(args) {
519
426
  return this.cfg.dockerHost ? ["-H", this.cfg.dockerHost, ...args] : args;
520
427
  }
521
- /** Build the `docker exec` argv for a command, applying cwd (`-w`) and per-command env (`-e K=V`).
522
- * argv elements are SEPARATE (no host-side shell join) — the command runs in the CONTAINER's `/bin/sh -c`. */
523
428
  execArgv(command, cwd, env) {
524
429
  const args = ["exec"];
525
430
  const dir = cwd != null ? this.resolve(cwd) : this.cwd;
526
431
  args.push("-w", dir);
527
- const mergedEnv = { ...(this.cfg.env ?? {}), ...(env ?? {}) }; // adapter secrets base; per-command wins
432
+ const mergedEnv = { ...(this.cfg.env ?? {}), ...(env ?? {}) };
528
433
  for (const [k, v] of Object.entries(mergedEnv))
529
- args.push("-e", `${k}=${v}`); // separate argv → no injection
530
- args.push(this.containerName, this.containerShell, "-c", command); // S1:探测后 bash 优先,无则 sh
434
+ args.push("-e", `${k}=${v}`);
435
+ args.push(this.containerName, this.containerShell, "-c", command);
531
436
  return args;
532
437
  }
533
- /** Like {@link execArgv} but for an internal FileSystem helper command (uses the workspace cwd + adapter env). */
534
438
  execRaw(command) {
535
439
  return this.execArgv(command);
536
440
  }
537
- /** Spawn the docker binary with argv; collect stdout/stderr (+ raw bytes when binary), bound by timeout/abort. */
538
441
  docker(args, opts) {
539
442
  return new Promise((resolve) => {
540
443
  let settled = false;
541
- // D5 pipe-destroy grace: armed by killGrace() on abort/timeout; destroys stdout/stderr pipes if `close`
542
- // never fires (a re-parented grandchild holding the FD). Cleared in finish() — the real `close` routes
543
- // through finish() — so the timer never leaks. Does NOT change the synchronous settle below.
544
444
  let forceSettleTimer;
545
445
  const child = this.spawnFn(this.cfg.dockerPath, this.withHostFlag(args), { stdio: ["ignore", "pipe", "pipe"] });
546
- // SIGKILL the child then arm the D5 pipe-destroy grace (only on the abort/timeout paths — a normal `close`
547
- // never calls this). Idempotent: only one timer is ever armed.
548
446
  const killGrace = () => {
549
447
  try {
550
448
  child.kill("SIGKILL");
551
449
  }
552
- catch { /* noop */ }
450
+ catch { }
553
451
  forceSettleTimer ??= armPipeDestroyGrace(child);
554
452
  };
555
- // 🔴 binary caveat (D2): this helper is SHARED by the model-facing TEXT exec (must be bounded — a
556
- // `yes`/`cat big` could OOM the durable worker) and by readBinaryFile (`docker exec ... cat`, opts.binary=true,
557
- // which reads stdoutBytes and MUST keep EVERY byte or binary/large reads silently corrupt). So we branch:
558
- // - binary=true → full Buffer[] + concat (byte-exact stdoutBytes; readBinaryFile path).
559
- // - binary=false → bounded rolling-TAIL for stdout (keep last 8MB + dropped-byte marker, O(maxBytes));
560
- // no text caller reads stdoutBytes, so it is derived from the bounded text (DockerRun.stdoutBytes is non-optional).
561
- // stderr is ALWAYS bounded — it never carries the binary payload (just cat's error text).
562
453
  const binary = !!opts?.binary;
563
- const outBufs = []; // binary path only: full bytes for stdoutBytes
564
- const outTail = binary ? undefined : new RollingTailBuffer(); // text path only: bounded stdout
454
+ const outBufs = [];
455
+ const outTail = binary ? undefined : new RollingTailBuffer();
565
456
  const errBuf = new RollingTailBuffer();
566
457
  const finish = (r) => {
567
458
  if (settled)
568
459
  return;
569
460
  settled = true;
570
461
  clearTimeout(timer);
571
- // D5 grace cleared in the `close` handler (pipes closed naturally), NOT here — abort/timeout reaches
572
- // finish() BEFORE `close`, so clearing here would cancel the grace before it can free a leaked FD.
573
462
  opts?.signal?.removeEventListener("abort", onAbort);
574
463
  resolve(r);
575
464
  };
@@ -590,33 +479,26 @@ export class RemoteLocalDockerExecutionEnv {
590
479
  child.stdout?.on("data", (d) => { if (binary)
591
480
  outBufs.push(d);
592
481
  else
593
- outTail.push(d); opts?.onStdout?.(d.toString()); }); // streaming callback gets the full chunk; only the buffered return is bounded
482
+ outTail.push(d); opts?.onStdout?.(d.toString()); });
594
483
  child.stderr?.on("data", (d) => { errBuf.push(d); opts?.onStderr?.(d.toString()); });
595
484
  child.on("error", (e) => finish({ ok: false, error: new ExecutionError("spawn_error", `docker spawn failed (is '${this.cfg.dockerPath}' installed?): ${e.message}`, e) }));
596
485
  child.on("close", (code, signal) => {
597
486
  if (forceSettleTimer)
598
- clearTimeout(forceSettleTimer); // `close` fired → pipes closed naturally; clear D5 grace
599
- // A signal-killed process (external SIGKILL/OOM) has code===null → derive the conventional 128+signo
600
- // (parity with execStream + the host adapter); NEVER report a killed command as a success exitCode 0.
487
+ clearTimeout(forceSettleTimer);
601
488
  const exitCode = code ?? (signal ? 128 + (signalNumber(signal) ?? 9) : 1);
602
489
  const err = errBuf.result();
603
490
  const stderr = markTruncated(err.text, err.droppedBytes);
604
491
  if (binary) {
605
- const outBytes = Buffer.concat(outBufs); // byte-exact: readBinaryFile decodes stdoutBytes; never truncate.
492
+ const outBytes = Buffer.concat(outBufs);
606
493
  finish(ok({ stdout: outBytes.toString("utf8"), stderr, exitCode, stdoutBytes: new Uint8Array(outBytes) }));
607
494
  }
608
495
  else {
609
- const out = outTail.result(); // text path: no stdoutBytes (readBinaryFile is the only reader, and it uses binary:true)
496
+ const out = outTail.result();
610
497
  finish(ok({ stdout: markTruncated(out.text, out.droppedBytes), stderr, exitCode }));
611
498
  }
612
499
  });
613
500
  });
614
501
  }
615
- /** Lazy connect for an exec path → ExecutionError on failure (fail closed, never throws). */
616
- /** S1([1870]):容器内**用户命令**的 shell。缺省 /bin/sh;首次连接后探测一次 `command -v bash`,
617
- * 有则切 bash(TB 实测 dash 三病:$'\t' 静默错字节/[[ ]] 127/进程替换)。镜像无 bash(alpine 裸底)
618
- * ⇒ 保 sh(修前字节形,诚实降级);探测自身失败同。容器**创建**引导命令(mkdir && tail)不走
619
- * 本 shell——无 bash 语法且探测尚不可能。 */
620
502
  containerShell = "/bin/sh";
621
503
  containerShellProbed = false;
622
504
  async probeContainerShell() {
@@ -624,23 +506,18 @@ export class RemoteLocalDockerExecutionEnv {
624
506
  return;
625
507
  this.containerShellProbed = true;
626
508
  try {
627
- // 🔴 NO withHostFlag here — `docker()` already prepends it. A second `-H` is not a harmless repeat: the
628
- // docker CLI rejects the whole command (`invalid argument … specify only one -H`), so on every
629
- // REMOTE-daemon container (`dockerHost` set — always, on the `remote-docker` arm) the probe used to fail
630
- // and silently degrade the user shell to /bin/sh for a reason unrelated to the image.
631
509
  const r = await this.docker(["exec", this.containerName, "/bin/sh", "-c", "command -v bash"], { timeoutMs: this.cfg.controlTimeoutMs });
632
510
  if (r.ok && r.value.exitCode === 0 && r.value.stdout.trim().length > 0)
633
511
  this.containerShell = "bash";
634
512
  }
635
513
  catch {
636
- /* 保 sh —— 探测绝不 fault 连接路径 */
637
514
  }
638
515
  }
639
516
  async ensureConnectedExec() {
640
517
  if (this.destroyed)
641
518
  return { ok: false, error: new ExecutionError("shell_unavailable", "execution env already destroyed") };
642
519
  if (this.containerId) {
643
- await this.probeContainerShell(); // S1:已连的腿也补探测(幂等一次)
520
+ await this.probeContainerShell();
644
521
  return ok(undefined);
645
522
  }
646
523
  const c = await this.connect();
@@ -649,7 +526,6 @@ export class RemoteLocalDockerExecutionEnv {
649
526
  await this.probeContainerShell();
650
527
  return ok(undefined);
651
528
  }
652
- /** Lazy connect for a FileSystem path → FileError on failure (fail closed, never throws). */
653
529
  async fsReady(p) {
654
530
  if (this.destroyed)
655
531
  return { ok: false, error: new FileError("unknown", "execution env already destroyed", this.resolve(p)) };
@@ -660,7 +536,6 @@ export class RemoteLocalDockerExecutionEnv {
660
536
  return { ok: false, error: new FileError("unknown", `local-docker connect failed: ${c.error.message}`, this.resolve(p), c.error) };
661
537
  return ok(undefined);
662
538
  }
663
- /** Map an ExecutionError from a docker CONTROL op (run/inspect) onto a RemoteExecutionError code. */
664
539
  dockerToRemoteError(e, fallback) {
665
540
  if (e.code === "timeout")
666
541
  return new RemoteExecutionError("timeout", e.message, e);
@@ -669,20 +544,12 @@ export class RemoteLocalDockerExecutionEnv {
669
544
  return new RemoteExecutionError(fallback, e.message, e);
670
545
  }
671
546
  }
672
- /** Strip anything but [A-Za-z0-9._-] from an id so it is safe in a docker container name. */
673
547
  function sanitizeId(id) {
674
548
  return id.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 48) || "task";
675
549
  }
676
550
  function errMsg(e) {
677
551
  return e instanceof Error ? e.message : String(e);
678
552
  }
679
- /**
680
- * `ExecutionEnvFactory` for the TOC `local-docker` backend (DUAL-MODE-DESIGN §5). Wiring this onto a deployment
681
- * makes its agent run each task in a per-task container on the worker's OWN docker daemon (isolation:true,
682
- * suspendable:false). Returns the env UNCONNECTED (lazy — the container starts on first fs/exec use); the Runner
683
- * owns the lifetime and calls `destroy()` (= `docker rm -f`) on task end. Folds the per-task `ctx.taskId`/
684
- * `ctx.sessionId` into the container name so two concurrent tasks never share a container.
685
- */
686
553
  export function localDockerExecutionEnvFactory(config, deps = {}) {
687
554
  return (ctx) => new RemoteLocalDockerExecutionEnv({ ...config, id: config.id ?? ctx.taskId ?? ctx.sessionId }, deps);
688
555
  }