@sema-agent/server 7.43.0 → 7.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (345) hide show
  1. package/USAGE.md +18 -0
  2. package/dist/adoption/plan.js +31 -192
  3. package/dist/adoption/quiesce.js +3 -86
  4. package/dist/adoption/runner.js +3 -140
  5. package/dist/adoption/sql.js +0 -74
  6. package/dist/adoption/wire.js +0 -80
  7. package/dist/approval-ask-machine.js +0 -75
  8. package/dist/approval-card.js +0 -323
  9. package/dist/approval-deny-reasons.js +0 -42
  10. package/dist/approval-hmac.js +0 -35
  11. package/dist/approval-reconciler.js +8 -166
  12. package/dist/approval.js +4 -67
  13. package/dist/audit.js +1 -44
  14. package/dist/auth-bridge.js +4 -47
  15. package/dist/auth-keys.js +0 -23
  16. package/dist/bake-runner/main.js +4 -65
  17. package/dist/bake-runner/protocol.js +3 -58
  18. package/dist/bake-runner/runner.js +5 -91
  19. package/dist/bench/l8/artifact.js +2 -61
  20. package/dist/bench/l8/escape.js +0 -25
  21. package/dist/bench/l8/index.js +0 -14
  22. package/dist/bench/l8/probes.js +0 -60
  23. package/dist/bench/l8/run-probes.js +2 -45
  24. package/dist/bench/s1/arms.js +14 -189
  25. package/dist/bench/s1/live-deps.js +11 -253
  26. package/dist/bench/s1/oracle.js +1 -36
  27. package/dist/bench/s1/repair-oracle-adapter.js +0 -30
  28. package/dist/bench/s1/reviewer.js +0 -34
  29. package/dist/bench/s1/row.js +0 -62
  30. package/dist/bench/s1/run-firm.js +4 -70
  31. package/dist/bench/s1/runner-ctx.js +0 -40
  32. package/dist/bench/s1/tasks.js +0 -115
  33. package/dist/boot/adoption.js +0 -20
  34. package/dist/boot/budget-tracing.js +3 -55
  35. package/dist/boot/config-center.js +43 -551
  36. package/dist/boot/coordinators.js +1 -69
  37. package/dist/boot/crash-last.js +0 -18
  38. package/dist/boot/deferred-sandbox-path-env.js +3 -119
  39. package/dist/boot/execution-env.js +5 -171
  40. package/dist/boot/governance-seams.js +0 -92
  41. package/dist/boot/leader.js +0 -69
  42. package/dist/boot/limit-sync.js +0 -2
  43. package/dist/boot/memory-boundary.js +3 -91
  44. package/dist/boot/org-memory.js +1 -25
  45. package/dist/boot/parked-revive-gate.js +0 -150
  46. package/dist/boot/permission-rules-audit.js +1 -56
  47. package/dist/boot/reapers.js +15 -273
  48. package/dist/boot/resolve-spec.js +9 -768
  49. package/dist/boot/retention-lane.js +4 -87
  50. package/dist/boot/runner-deps.js +3 -297
  51. package/dist/boot/runtime-caps.js +0 -40
  52. package/dist/boot/session-faces.js +2 -133
  53. package/dist/boot/shutdown.js +9 -83
  54. package/dist/boot/side-query-lane.js +2 -137
  55. package/dist/boot/stores.js +15 -215
  56. package/dist/boot/task-list-lane.js +0 -18
  57. package/dist/boot/webfetch-summarize-lane.js +0 -47
  58. package/dist/boot/workflow-orchestration.js +3 -99
  59. package/dist/boot-reclaim.js +0 -20
  60. package/dist/bounded-session-map.js +0 -19
  61. package/dist/brain.js +2 -139
  62. package/dist/budget.js +9 -244
  63. package/dist/capabilities/builtin-tools.js +0 -3
  64. package/dist/capabilities/center-plugins.js +6 -51
  65. package/dist/capabilities/center-prompts.js +6 -71
  66. package/dist/capabilities/code-review-council.js +7 -40
  67. package/dist/capabilities/collab-workflows.js +1 -44
  68. package/dist/capabilities/hands-lane.js +0 -65
  69. package/dist/capabilities/memory-notice.js +0 -70
  70. package/dist/capabilities/prompt.js +0 -14
  71. package/dist/capabilities/prompts/code-review.js +0 -14
  72. package/dist/capabilities/prompts/identity.js +0 -6
  73. package/dist/capabilities/prompts/team.js +0 -4
  74. package/dist/capabilities/repo-tools.js +0 -34
  75. package/dist/capabilities/sandbox-file-send.js +3 -92
  76. package/dist/capabilities/scenarios.d.ts +0 -1
  77. package/dist/capabilities/scenarios.js +18 -279
  78. package/dist/capabilities/select-environment-tool.js +0 -32
  79. package/dist/capabilities/send-user-file-tool.js +2 -66
  80. package/dist/capabilities/skills.d.ts +1 -2
  81. package/dist/capabilities/skills.js +3 -21
  82. package/dist/capabilities/team.d.ts +3 -11
  83. package/dist/capabilities/team.js +3 -51
  84. package/dist/capabilities/tool-defer.js +0 -3
  85. package/dist/config-center/apply-effective.js +15 -330
  86. package/dist/config-center/apply-ledger.js +5 -38
  87. package/dist/config-center/facade.js +0 -41
  88. package/dist/config-center/hot-keys-registry.js +0 -20
  89. package/dist/config-center/http-client.js +2 -125
  90. package/dist/config-center/mcp-revocation.js +2 -34
  91. package/dist/config-center/read-face.js +0 -59
  92. package/dist/config-center/restart-signal.js +1 -85
  93. package/dist/config-center/skills-mcp.d.ts +1 -1
  94. package/dist/config-center/skills-mcp.js +9 -76
  95. package/dist/config-center/stage-limits.js +8 -35
  96. package/dist/config-invariants.js +0 -16
  97. package/dist/config-lkg.js +0 -42
  98. package/dist/config-provider.js +3 -186
  99. package/dist/config-types.js +0 -5
  100. package/dist/config.js +107 -1145
  101. package/dist/degenerate-instrument.js +3 -67
  102. package/dist/deployment-governance.js +0 -124
  103. package/dist/digest-form.js +0 -12
  104. package/dist/elicitation.js +3 -86
  105. package/dist/env-facts.js +7 -75
  106. package/dist/fleet/fleet-bus.js +34 -507
  107. package/dist/fleet/fleet-reconciler.js +9 -149
  108. package/dist/fleet/fleet-terminal-window.js +10 -178
  109. package/dist/fleet/subagent-tail-bus.js +3 -72
  110. package/dist/fleet-client.js +10 -70
  111. package/dist/fleet-lease.js +5 -79
  112. package/dist/git-api-kind.js +0 -3
  113. package/dist/governance-ask-marks.js +2 -78
  114. package/dist/hooks/branch-transcript.js +0 -74
  115. package/dist/hooks/cc-agent-hook-prompt.js +0 -29
  116. package/dist/hooks/cc-stop-prompt.js +1 -46
  117. package/dist/hooks/hook-llm.js +1 -53
  118. package/dist/hooks/hook-runner.js +20 -414
  119. package/dist/http/active-run-conflict.js +4 -129
  120. package/dist/http/cursor-fingerprint.d.ts +5 -0
  121. package/dist/http/cursor-fingerprint.js +5 -0
  122. package/dist/http/idempotency.js +0 -37
  123. package/dist/http/principal-gate.js +3 -40
  124. package/dist/http/route-ctx.js +0 -9
  125. package/dist/http/routes/a2a-serve.js +7 -319
  126. package/dist/http/routes/admin-config-refresh.js +0 -4
  127. package/dist/http/routes/admin-drain.js +0 -10
  128. package/dist/http/routes/adoption.js +1 -29
  129. package/dist/http/routes/agents-roster.js +1 -47
  130. package/dist/http/routes/approvals-assistant.js +32 -395
  131. package/dist/http/routes/attachments.js +4 -22
  132. package/dist/http/routes/capabilities.js +3 -420
  133. package/dist/http/routes/diagnostics.js +1 -81
  134. package/dist/http/routes/fleet.js +9 -185
  135. package/dist/http/routes/images.js +14 -239
  136. package/dist/http/routes/leader.js +0 -13
  137. package/dist/http/routes/memory-bundle.js +1 -61
  138. package/dist/http/routes/memory-policy.js +9 -99
  139. package/dist/http/routes/notify-wake.js +3 -37
  140. package/dist/http/routes/observability.js +2 -19
  141. package/dist/http/routes/retention-ops.js +2 -34
  142. package/dist/http/routes/rules.js +1 -93
  143. package/dist/http/routes/runs.js +64 -873
  144. package/dist/http/routes/session-sync.js +19 -258
  145. package/dist/http/routes/sessions-list.js +17 -43
  146. package/dist/http/routes/sessions.js +34 -226
  147. package/dist/http/routes/shared-memory.js +5 -36
  148. package/dist/http/routes/side-query.js +1 -87
  149. package/dist/http/routes/tasks.js +52 -722
  150. package/dist/http/routes/trace-usage.js +32 -195
  151. package/dist/http/routes/workflows.js +19 -195
  152. package/dist/http/run-meta.js +0 -6
  153. package/dist/http/send.js +0 -32
  154. package/dist/http/server.js +92 -1597
  155. package/dist/http/sse-lifecycle.js +2 -13
  156. package/dist/http/sse-log.js +3 -48
  157. package/dist/http/tar.js +5 -21
  158. package/dist/http/verify-rounds.js +0 -5
  159. package/dist/http/wire-gate.js +0 -9
  160. package/dist/http/workspace-content.js +0 -10
  161. package/dist/images/bake-validate.js +1 -70
  162. package/dist/images/manifest.js +1 -6
  163. package/dist/index.js +0 -21
  164. package/dist/key-resolver.js +2 -17
  165. package/dist/leader/diffout.js +1 -20
  166. package/dist/leader/diffup.js +0 -47
  167. package/dist/leader/endpoint.js +2 -57
  168. package/dist/leader/fanout.js +3 -45
  169. package/dist/leader/grader-env-factory.js +3 -72
  170. package/dist/leader/leader.js +5 -156
  171. package/dist/leader/merge.js +8 -115
  172. package/dist/leader/planner.js +3 -54
  173. package/dist/leader/repair-oracle.js +1 -60
  174. package/dist/leader/repair-wire.js +2 -79
  175. package/dist/leader/wire.js +8 -307
  176. package/dist/lsp/e2b-bridge.js +4 -64
  177. package/dist/lsp/e2b-manager.js +6 -94
  178. package/dist/lsp/lsp-frames.js +0 -12
  179. package/dist/lsp/manager.js +4 -96
  180. package/dist/lsp/ws-transport.js +5 -55
  181. package/dist/lsp-evict.js +1 -15
  182. package/dist/main.js +57 -765
  183. package/dist/memory-bundle-engine.js +0 -55
  184. package/dist/memory-export.js +0 -4
  185. package/dist/memory-posture.js +1 -15
  186. package/dist/memory-scope.js +11 -148
  187. package/dist/memory-sync-client.js +2 -44
  188. package/dist/memory-sync.js +1 -80
  189. package/dist/model-select.js +3 -80
  190. package/dist/observability/cost-quota.js +1 -17
  191. package/dist/observability/cost-taxonomy.js +0 -34
  192. package/dist/observability/fail-open.js +7 -86
  193. package/dist/observability/logger.js +0 -6
  194. package/dist/observability/metrics.js +0 -94
  195. package/dist/observability/otel-exporter.js +3 -13
  196. package/dist/observability/principal-context.js +0 -9
  197. package/dist/observability/prompt-manifest.js +1 -37
  198. package/dist/observability/rate-limit.js +0 -4
  199. package/dist/observability/secret-env-scrub.js +2 -56
  200. package/dist/observability/tool-trace.js +1 -70
  201. package/dist/orchestration/hardened-vm-runner.js +4 -118
  202. package/dist/orchestration/hardened-vm-worker-runner.js +1 -26
  203. package/dist/orchestration/hardened-vm-worker.js +0 -27
  204. package/dist/orchestration/subagent-steer.js +1 -45
  205. package/dist/orchestration/workflow-agent-steer.js +1 -80
  206. package/dist/orchestration/workflow-completion-inbox.js +32 -285
  207. package/dist/orchestration/workflow-notify-journal.js +16 -259
  208. package/dist/org-memory-admission.js +3 -47
  209. package/dist/parent-watch.js +2 -48
  210. package/dist/parked-decide.js +1 -109
  211. package/dist/per-task-image.js +0 -57
  212. package/dist/plan-cache-probe.js +3 -27
  213. package/dist/plugins/adoption-log-sql.js +2 -119
  214. package/dist/plugins/approval-ask-store-memory.js +3 -38
  215. package/dist/plugins/approval-ask-store-sql.js +6 -188
  216. package/dist/plugins/approval-exemption-store.js +2 -28
  217. package/dist/plugins/background-agent-store-sql.js +4 -105
  218. package/dist/plugins/background-shell-support.js +14 -122
  219. package/dist/plugins/blob-backend.js +6 -169
  220. package/dist/plugins/breaker-state-sql.js +8 -46
  221. package/dist/plugins/caching-session-store.js +4 -106
  222. package/dist/plugins/checkpoint-store-sql.js +18 -505
  223. package/dist/plugins/e2b-orphan-reclaim.js +0 -45
  224. package/dist/plugins/file-outcome-sink.js +0 -9
  225. package/dist/plugins/file-resume-anchor-store.js +4 -43
  226. package/dist/plugins/file-run-store.js +26 -364
  227. package/dist/plugins/file-snapshot-store-sql.js +10 -181
  228. package/dist/plugins/fork-routing-session-store.js +8 -111
  229. package/dist/plugins/host-platform.js +2 -91
  230. package/dist/plugins/image-bake-store-sql.js +7 -250
  231. package/dist/plugins/image-index-sql.js +4 -123
  232. package/dist/plugins/k8s-bg-scripts.js +4 -88
  233. package/dist/plugins/k8s-exec-protocol.js +0 -41
  234. package/dist/plugins/leader-run-store-sql.js +0 -101
  235. package/dist/plugins/local-checkpoint-store.js +8 -128
  236. package/dist/plugins/local-session-store.js +34 -296
  237. package/dist/plugins/local-task-attachment-store.js +2 -16
  238. package/dist/plugins/mailbox-store-sql.js +8 -65
  239. package/dist/plugins/memory-embedder-fingerprint.js +5 -166
  240. package/dist/plugins/memory-embedder.js +4 -70
  241. package/dist/plugins/memory-engine-pg.js +6 -170
  242. package/dist/plugins/memory-engine-tidb.js +7 -155
  243. package/dist/plugins/memory-engine-vector-util.js +0 -10
  244. package/dist/plugins/memory-key-guards.js +0 -34
  245. package/dist/plugins/memory-origin-law.js +0 -187
  246. package/dist/plugins/memory-resume-anchor-store.js +0 -17
  247. package/dist/plugins/memory-run-store.js +12 -89
  248. package/dist/plugins/memory-session-policy-store.js +0 -17
  249. package/dist/plugins/memory-sync-store-pg.js +4 -49
  250. package/dist/plugins/memory-sync-store-tidb.js +3 -35
  251. package/dist/plugins/outcome-ledger-sql.js +3 -97
  252. package/dist/plugins/permission-rule-store-file.js +5 -133
  253. package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
  254. package/dist/plugins/permission-rule-store-sql.js +50 -307
  255. package/dist/plugins/pg-cost-quota.js +0 -7
  256. package/dist/plugins/pg-pool.js +0 -92
  257. package/dist/plugins/pg-rate-limiter.js +2 -13
  258. package/dist/plugins/pg-safe-json.js +4 -40
  259. package/dist/plugins/pg-session-storage.js +25 -189
  260. package/dist/plugins/posix-shell-fs.js +1 -31
  261. package/dist/plugins/remote-env-adb.js +12 -101
  262. package/dist/plugins/remote-env-e2b.js +40 -371
  263. package/dist/plugins/remote-env-file-error.js +0 -37
  264. package/dist/plugins/remote-env-host.js +63 -483
  265. package/dist/plugins/remote-env-k8s.js +32 -326
  266. package/dist/plugins/remote-env-local-docker.js +24 -157
  267. package/dist/plugins/remote-env-ssh.js +20 -128
  268. package/dist/plugins/remote-scratchpad.js +2 -32
  269. package/dist/plugins/remote-shell.js +1 -32
  270. package/dist/plugins/resume-anchor-store-sql.js +0 -11
  271. package/dist/plugins/retention-lane-store-sql.js +0 -108
  272. package/dist/plugins/retention-store-sql.js +4 -383
  273. package/dist/plugins/roster-store-sql.js +0 -55
  274. package/dist/plugins/run-store-sql.js +14 -251
  275. package/dist/plugins/s3-presign.js +2 -49
  276. package/dist/plugins/scheduler-support.js +3 -80
  277. package/dist/plugins/send-file-ledger.js +4 -53
  278. package/dist/plugins/send-user-file.js +4 -94
  279. package/dist/plugins/session-placement.js +1 -89
  280. package/dist/plugins/session-policy-store-sql.js +3 -81
  281. package/dist/plugins/session-store.js +0 -57
  282. package/dist/plugins/shared-memory-store-sql.js +4 -167
  283. package/dist/plugins/sql-driver.js +0 -17
  284. package/dist/plugins/sql-errors.js +0 -7
  285. package/dist/plugins/sql-escape.js +0 -8
  286. package/dist/plugins/sql-row-helpers.js +0 -25
  287. package/dist/plugins/store-backend.js +42 -222
  288. package/dist/plugins/store-contracts.js +2 -46
  289. package/dist/plugins/task-attachment-store.js +3 -49
  290. package/dist/plugins/task-list-store-sql.js +0 -79
  291. package/dist/plugins/tidb-cost-quota.js +1 -4
  292. package/dist/plugins/tidb-pool.js +1 -207
  293. package/dist/plugins/tidb-rate-limiter.js +3 -9
  294. package/dist/plugins/tidb-session-storage.js +4 -70
  295. package/dist/plugins/tidb-session-store.js +19 -341
  296. package/dist/plugins/tool-result-store-sql.js +4 -179
  297. package/dist/plugins/usage-window-store-sql.js +0 -8
  298. package/dist/plugins/web-search.js +10 -132
  299. package/dist/plugins/workflow-journal-store-sql.js +2 -58
  300. package/dist/plugins/workflow-run-store-sql.js +6 -91
  301. package/dist/plugins/worktree-isolation.js +6 -126
  302. package/dist/plugins/write-behind-counter.js +16 -75
  303. package/dist/principal-jwt.js +5 -60
  304. package/dist/project-memory.js +15 -146
  305. package/dist/prompts-domain-validate.js +1 -51
  306. package/dist/question.js +2 -111
  307. package/dist/resource-suspend.js +0 -18
  308. package/dist/router/route-orchestration.js +0 -77
  309. package/dist/rules-consent.d.ts +44 -5
  310. package/dist/rules-consent.js +75 -215
  311. package/dist/run-local.js +14 -381
  312. package/dist/runs.js +29 -617
  313. package/dist/runtime-caps-resolver.js +6 -132
  314. package/dist/runtime-governance.js +1 -232
  315. package/dist/sandbox-pkg-source.js +0 -37
  316. package/dist/sealed-key.js +3 -68
  317. package/dist/security.js +6 -291
  318. package/dist/session-leaf-bus.js +0 -32
  319. package/dist/session-sync-content.js +1 -69
  320. package/dist/session-sync-kernel.js +3 -60
  321. package/dist/session-sync.js +3 -66
  322. package/dist/session-titler.js +7 -41
  323. package/dist/session-watch.js +9 -73
  324. package/dist/shared-memory-scope-authorizer.js +0 -18
  325. package/dist/sighup-idle.js +1 -12
  326. package/dist/spec-fields.js +4 -143
  327. package/dist/store-live-probe.js +3 -49
  328. package/dist/task-a2a.js +1 -126
  329. package/dist/task-cwd.js +1 -103
  330. package/dist/task-mcp.js +1 -89
  331. package/dist/task-settings.js +10 -300
  332. package/dist/task-workflow.js +8 -75
  333. package/dist/tool-approval.d.ts +28 -1
  334. package/dist/tool-approval.js +70 -1430
  335. package/dist/trace/artifacts.js +5 -20
  336. package/dist/trace/engine-notice-wire.js +6 -138
  337. package/dist/trace/ledger-sink.js +7 -96
  338. package/dist/trace/project.js +4 -448
  339. package/dist/trace/redact.js +9 -77
  340. package/dist/turn-activity.js +1 -27
  341. package/dist/usage-analytics.js +4 -34
  342. package/dist/wall-clock-jump-guard.js +1 -68
  343. package/package.json +3 -3
  344. package/dist/capabilities/scenario-alias.d.ts +0 -27
  345. package/dist/capabilities/scenario-alias.js +0 -61
@@ -1,24 +1,3 @@
1
- /**
2
- * `RemoteSshExecutionEnv` — the SSH implementation of core's `RemoteExecutionEnv` seam (design/61). A PEER
3
- * adapter to the E2B one (NOT chained): `docker(agent) → [SSH adapter] → real machine`. Use case: an AI
4
- * orchestrating batch deployment over SSH-reachable hosts.
5
- *
6
- * 🔴 **SSH targets a REAL system, not a disposable sandbox** (design/61 §5):
7
- * - `capabilities = { isolation: false, suspendable: false }` — actions are PERMANENT, no snapshot. The
8
- * orchestrator/leader tightens autonomy + the design/37 policy gate + HITL on a non-isolated env (design/53).
9
- * - `destroy()` / `cleanup()` **close the SSH connection** — they do NOT destroy the machine (vs E2B's
10
- * teardown). Anything the agent created on the host PERSISTS (cleanup is the gate/operator's job).
11
- * - `suspendVM`/`resumeVM`/`postResumeInit` → typed `"unsupported"` (a real machine isn't snapshotable;
12
- * design/45 durable suspend N/A). `reconnect` re-establishes the connection.
13
- *
14
- * 🔴 **Credential isolation (design/61 §5 #2 / design/53):** the private key is held by the control plane and
15
- * injected to the ADAPTER as a secret env (`SecretRef.destination:"adapter"`) — it NEVER enters the workspace
16
- * / agent-reachable surface. The agent operates via exec/sftp; it cannot read the key.
17
- *
18
- * Liveness: every SSH op is a network round-trip that can hang — `connect` is wrapped in `withRetry` (idempotent
19
- * establishment only), exec/sftp carry a bounded timeout, and a hang surfaces a typed retryable error (the
20
- * shared timeout/liveness contract, reused).
21
- */
22
1
  import path from "node:path";
23
2
  import { Client } from "ssh2";
24
3
  import { shellQuote, kindFromMode } from "./remote-shell.js";
@@ -32,30 +11,16 @@ const unsupported = (op) => ({
32
11
  ok: false,
33
12
  error: new RemoteExecutionError("unsupported", `${op} is not supported on an SSH target (real machine — not snapshotable; capabilities.suspendable=false)`),
34
13
  });
35
- /** ssh2's ClientChannel "close" event gives the killing signal's BARE POSIX name (e.g. "KILL"), unlike Node's
36
- * own `child.on("close")` which gives `NodeJS.Signals` names (e.g. "SIGKILL") — re-prefix before looking it
37
- * up in the shared, authoritative signal table (host-platform.ts `signalNumber`; single pattern-home, no
38
- * hand-written table here). `?? 9` matches the convention every other exec adapter uses for an unmapped name. */
39
14
  function exitCodeForSignalKill(signal) {
40
15
  return 128 + (signalNumber(`SIG${signal}`) ?? 9);
41
16
  }
42
17
  export class RemoteSshExecutionEnv {
43
18
  capabilities = { isolation: false, suspendable: false };
44
- /** Working directory; relative paths resolve against it (ExecutionEnv contract). Starts at mountPath. */
45
19
  cwd;
46
20
  conn;
47
21
  sftpPromise;
48
22
  destroyed = false;
49
- /** [1165] 代际闸:destroy/reconnect 各自 +1。逻辑连接操作(startConnect)起飞时抓拍一次并贯穿全部
50
- * 重试轮(四轮复审:抓拍若在 connectOnce 内,被顶替代的下一轮重试会抓到**新**代际混进新代)——
51
- * 每轮起跳先查代际/destroyed(陈旧=不再开 socket),ready 到达再查一次(在飞握手 late-ready=end
52
- * 不赋 conn)。关死「A 在飞→reconnect 起 B→A 竞后抢占/成孤儿活连接」的全部竞窗。 */
53
23
  connEpoch = 0;
54
- /** [1165] 四轮复审 single-flight:connect()/reconnect()/懒连接三路共用一条代际键在飞记录——任何一路
55
- * 在飞时其余路 join 同一 promise(同代际),绝不并起两条连接;settle 时按记录身份清(旧败诺清不掉
56
- * 新记录)。五轮复审:①记录带 kind——并发 reconnect join 已在飞的 replace 记录(不再各自 bump 代际
57
- * =重连风暴);②共享尝试**不携带任何 caller 信号**(首创者的 pre-abort 不得毒化 joiner;单 caller
58
- * 取消只退它自己的等待,底层握手余命 ≤ readyTimeout 有界,彻底取消归 destroy 代际闸)。 */
59
24
  inFlight;
60
25
  cfg;
61
26
  injectedConnectOnce;
@@ -81,20 +46,13 @@ export class RemoteSshExecutionEnv {
81
46
  workspaceHandle() {
82
47
  return { sandboxId: `${this.cfg.host}:${this.cfg.port}`, provider: PROVIDER, mountPath: this.cwd };
83
48
  }
84
- // ─────────────────────────────── lifecycle ───────────────────────────────
85
49
  async connect(config) {
86
- // [1165] 三轮复审:destroy 后一律不复活(显式 connect/reconnect 同禁)——destroyed env 是 teardown
87
- // 终态,复活=任务清理后还能执行命令/留活连接(资源与安全双漏)。恢复流程走工厂新铸实例。
88
50
  if (this.destroyed)
89
51
  return { ok: false, error: new RemoteExecutionError("connect_failed", "SSH env destroyed — create a fresh env instead of reviving") };
90
- // 已连=幂等成功(四轮复审:旧形二次 connect 会并起第二条连接顶掉第一条=孤儿活连接)。
91
52
  if (this.conn)
92
53
  return ok(this.workspaceHandle());
93
- // 五轮复审:caller 信号只管 caller 自己的等待(race),共享尝试不沾——pre-abort 的首创者不得
94
- // 毒化同拍 joiner,反向 pre-abort 的 joiner 也必须即时退出。
95
54
  return this.raceConnAbort(this.startConnect("establish"), config?.abortSignal);
96
55
  }
97
- /** 五轮复审:per-caller 可中止等待(共享诺不动,只退本 caller)。 */
98
56
  async raceConnAbort(shared, signal) {
99
57
  if (!signal)
100
58
  return shared;
@@ -112,15 +70,12 @@ export class RemoteSshExecutionEnv {
112
70
  return { ok: false, error: new RemoteExecutionError("aborted", "connect aborted while waiting") };
113
71
  return raced;
114
72
  }
115
- /** [1165] 四轮复审:三路(connect/reconnect/懒连接)唯一的连接发起点——同代际在飞记录 join,settle
116
- * 身份清;代际抓拍一次贯穿全部重试轮,每轮起跳先查(陈旧/destroyed=不再开 socket,用不可重试的
117
- * aborted 码即刻收敛 withRetry)。五轮复审:共享尝试零 caller 信号(取消面全在 per-caller race)。 */
118
73
  startConnect(kind) {
119
74
  if (this.inFlight && this.inFlight.epoch === this.connEpoch)
120
75
  return this.inFlight.promise;
121
76
  const epoch = this.connEpoch;
122
77
  const policy = {
123
- retryableCodes: ["connect_failed", "timeout"], // NOT auth_failed (permanent — retrying a rejected key burns budget)
78
+ retryableCodes: ["connect_failed", "timeout"],
124
79
  maxAttempts: this.cfg.retry.maxAttempts,
125
80
  backoffMs: (failed) => this.cfg.retry.backoffMs * failed,
126
81
  };
@@ -135,28 +90,22 @@ export class RemoteSshExecutionEnv {
135
90
  const record = { epoch, kind, promise: raw };
136
91
  record.promise = raw.then((r) => {
137
92
  if (this.inFlight === record)
138
- this.inFlight = undefined; // 身份清:旧败诺清不掉新记录
93
+ this.inFlight = undefined;
139
94
  return r;
140
95
  });
141
96
  this.inFlight = record;
142
97
  return record.promise;
143
98
  }
144
- /** [1165] exec/execStream/sftp 共用的懒连接入口。已连=零开销直过;未连=走完整 connect()(含重试策略)。
145
- * caller 的 signal 只中止**它自己的等待**(共享在飞尝试继续伺服其他等待者;彻底取消归 destroy 的代际
146
- * 闸管,handshake 余命 ≤ readyTimeout 有界)。 */
147
99
  async ensureConnected(signal) {
148
- // destroyed 先于 conn 查(三轮复审:teardown 竞窗里 conn 可能在 destroy 后才被 ready 赋上——那条
149
- // 连接由 ready 守卫立即 end,这里也不得放行)。
150
100
  if (this.destroyed)
151
101
  return { ok: false, error: new ExecutionError("shell_unavailable", "SSH env destroyed — no lazy reconnect after teardown") };
152
102
  if (signal?.aborted)
153
103
  return { ok: false, error: new ExecutionError("aborted", "aborted before SSH lazy-connect") };
154
104
  if (this.conn)
155
105
  return ok(undefined);
156
- const shared = this.startConnect("establish"); // 四轮复审:single-flight join(与显式 connect/reconnect 同记录)
106
+ const shared = this.startConnect("establish");
157
107
  let r;
158
108
  if (signal) {
159
- // 可中止等待:只退出本 caller 的 await,不动共享在飞记录(其他等待者照常拿结果)。
160
109
  let onAbort;
161
110
  const aborted = new Promise((res) => {
162
111
  onAbort = () => res("aborted");
@@ -175,8 +124,6 @@ export class RemoteSshExecutionEnv {
175
124
  if (r.ok) {
176
125
  if (this.conn)
177
126
  return ok(undefined);
178
- // 旗到手但连接已不在(建后即断竞窗)——在飞记录 settle 时已自清;诚实报不可用,下次调用自然
179
- // 重建(不递归重连:防断连风暴无界重试)。
180
127
  return { ok: false, error: new ExecutionError("shell_unavailable", "SSH connection dropped right after connect") };
181
128
  }
182
129
  if (r.error.code === "aborted")
@@ -184,7 +131,7 @@ export class RemoteSshExecutionEnv {
184
131
  return { ok: false, error: new ExecutionError("shell_unavailable", `SSH lazy-connect failed: ${r.error.message}`, r.error) };
185
132
  }
186
133
  connectOnce(signal, opEpoch) {
187
- const epoch = opEpoch ?? this.connEpoch; // 代际=逻辑连接操作抓拍(四轮复审:重试轮不得混进新代)
134
+ const epoch = opEpoch ?? this.connEpoch;
188
135
  return new Promise((resolve) => {
189
136
  if (signal?.aborted)
190
137
  return resolve({ ok: false, error: new RemoteExecutionError("aborted", "connect aborted") });
@@ -199,19 +146,15 @@ export class RemoteSshExecutionEnv {
199
146
  };
200
147
  c.on("ready", () => {
201
148
  if (this.destroyed || epoch !== this.connEpoch) {
202
- // [1165] 三轮复审 teardown/代际竞窗:destroy 或 reconnect 之后才 ready 的旧代连接——立即断掉
203
- // 且不赋 conn(否则 sole destroy 之后留一条活连接+可执行面,或旧代顶掉新代)。
204
149
  try {
205
150
  c.end();
206
151
  }
207
- catch { /* noop */ }
152
+ catch { }
208
153
  resolve({ ok: false, error: new RemoteExecutionError("connect_failed", this.destroyed ? "SSH env destroyed while connecting" : "SSH connection superseded while connecting (reconnect)") });
209
154
  return;
210
155
  }
211
156
  this.conn = c;
212
157
  this.sftpPromise = undefined;
213
- // If this live connection later drops, clear our handles so the next exec()/fs op fails closed with
214
- // "shell_unavailable" (council #3) instead of hitting a dead-but-truthy client with a raw ssh2 error.
215
158
  c.on("close", () => {
216
159
  if (this.conn === c) {
217
160
  this.conn = undefined;
@@ -224,7 +167,6 @@ export class RemoteSshExecutionEnv {
224
167
  c.end();
225
168
  }
226
169
  catch {
227
- /* noop */
228
170
  }
229
171
  resolve({ ok: false, error: this.connErr(e) });
230
172
  });
@@ -245,29 +187,24 @@ export class RemoteSshExecutionEnv {
245
187
  async postResumeInit() {
246
188
  return unsupported("postResumeInit");
247
189
  }
248
- /** Re-establish a dropped connection (supported — unlike suspend, reconnect is idempotent establishment). */
249
190
  async reconnect(_sessionToken) {
250
191
  if (this.destroyed)
251
192
  return { ok: false, error: new RemoteExecutionError("connect_failed", "SSH env destroyed — create a fresh env instead of reviving") };
252
- // 五轮复审:并发 reconnect join 已在飞的 replace 记录(不再各自 bump 代际=每路一条握手的重连风暴);
253
- // 只有「无在飞 replace」才推代际顶掉旧 establish/旧连接。
254
193
  if (this.inFlight && this.inFlight.epoch === this.connEpoch && this.inFlight.kind === "replace")
255
194
  return this.inFlight.promise;
256
- this.connEpoch++; // [1165] 在飞旧代自灭(每轮起跳查 + ready 守卫双闸)
195
+ this.connEpoch++;
257
196
  this.conn?.end();
258
197
  this.conn = undefined;
259
198
  this.sftpPromise = undefined;
260
- return this.startConnect("replace"); // 四轮复审:发起点唯一——重连期间的 exec/sftp join 同一记录
199
+ return this.startConnect("replace");
261
200
  }
262
- /** Close the SSH connection. Does NOT destroy the machine (real system) — idempotent, never throws. */
263
201
  async destroy() {
264
- this.destroyed = true; // [1165] 懒连接不复活已拆毁的 env(fail-closed 契约保持)
265
- this.connEpoch++; // 在飞连接每轮起跳查+ready 守卫自灭(handshake 余命 ≤ readyTimeout,有界)
202
+ this.destroyed = true;
203
+ this.connEpoch++;
266
204
  try {
267
205
  this.conn?.end();
268
206
  }
269
207
  catch {
270
- /* best-effort */
271
208
  }
272
209
  this.conn = undefined;
273
210
  this.sftpPromise = undefined;
@@ -275,10 +212,7 @@ export class RemoteSshExecutionEnv {
275
212
  async cleanup() {
276
213
  return this.destroy();
277
214
  }
278
- // ─────────────────────────────── exec ───────────────────────────────
279
215
  async exec(command, options) {
280
- // [1165] lazy-connect:core prepare-task 铸 env 不显式 connect(k8s 道同前提)——首用自动建连;
281
- // caller 的 abortSignal 覆盖建连等待段(pre-abort 与等待中 abort 都即时退出)。
282
216
  const ready = await this.ensureConnected(options?.abortSignal);
283
217
  if (!ready.ok)
284
218
  return ready;
@@ -287,10 +221,6 @@ export class RemoteSshExecutionEnv {
287
221
  return { ok: false, error: new ExecutionError("shell_unavailable", "SSH not connected — call connect() first") };
288
222
  const timeoutMs = options?.timeout != null ? options.timeout * 1000 : this.cfg.commandTimeoutMs;
289
223
  return new Promise((resolve) => {
290
- // D2: bounded rolling-TAIL (core's RollingTailBuffer) so a model-driven `yes`/`cat big` can't OOM the
291
- // durable worker — keep only the last 8MB (tail = errors+exit) + a dropped-byte marker, O(maxBytes) memory.
292
- // SAFE: this is the model-facing TEXT exec path only; readBinaryFile uses SFTP (sftp.readFile), a separate
293
- // byte-exact path that NEVER flows through these buffers.
294
224
  const outBuf = new RollingTailBuffer();
295
225
  const errBuf = new RollingTailBuffer();
296
226
  let settled = false;
@@ -303,18 +233,14 @@ export class RemoteSshExecutionEnv {
303
233
  options?.abortSignal?.removeEventListener("abort", onAbort);
304
234
  conn.removeListener("close", onConnClose);
305
235
  try {
306
- streamRef?.close(); // timeout/abort: close the channel so the remote command stops (council #1). No-op if already closed.
236
+ streamRef?.close();
307
237
  }
308
238
  catch {
309
- /* noop */
310
239
  }
311
240
  resolve(r);
312
241
  };
313
242
  const timer = setTimeout(() => finish({ ok: false, error: new ExecutionError("timeout", `ssh exec exceeded ${timeoutMs}ms (provider/command hang)`) }), timeoutMs);
314
243
  const onAbort = () => finish({ ok: false, error: new ExecutionError("aborted", "ssh exec aborted") });
315
- // Transport mapping: the TCP/SSH transport dying mid-command must NOT look like success or a vague
316
- // failure. Typed "transport_lost" since core 1.89 (outcome UNKNOWN — reconnect, then retry
317
- // only per the op's idempotency); the close-without-exit-status path below catches the same condition.
318
244
  const onConnClose = () => finish({ ok: false, error: new ExecutionError("transport_lost", "ssh transport lost mid-command (connection closed before exit status) — reconnect + retry per idempotency") });
319
245
  conn.once("close", onConnClose);
320
246
  if (options?.abortSignal) {
@@ -329,30 +255,23 @@ export class RemoteSshExecutionEnv {
329
255
  streamRef = stream;
330
256
  if (settled) {
331
257
  try {
332
- stream.close(); // already timed out/aborted before the exec callback fired → don't leak the channel
258
+ stream.close();
333
259
  }
334
260
  catch {
335
- /* noop */
336
261
  }
337
262
  return;
338
263
  }
339
264
  stream
340
265
  .on("data", (d) => {
341
- outBuf.push(d); // streaming callback still gets the full chunk; only the buffered return value is bounded
266
+ outBuf.push(d);
342
267
  options?.onStdout?.(d.toString());
343
268
  })
344
269
  .on("close", (code, signal) => {
345
- // code==null without a kill signal = the channel died without delivering an exit status (transport drop),
346
- // NOT a successful exit — was silently mapped to exitCode 0 before ([R78]#1).
347
270
  if (code == null && !signal) {
348
271
  return finish({ ok: false, error: new ExecutionError("transport_lost", "ssh channel closed without exit status (transport lost / connection dropped)") });
349
272
  }
350
273
  const out = outBuf.result();
351
274
  const err = errBuf.result();
352
- // code==null WITH a signal = the remote command was killed by that signal — conventional 128+signo,
353
- // NOT a fake success 0 ([R78]#2; was silently mapped to exitCode 0 before this fix). The `: 1`
354
- // arm is unreachable (the transport_lost return above already covers code==null && !signal) but
355
- // keeps the type honest without a non-null assertion.
356
275
  const exitCode = code ?? (signal ? exitCodeForSignalKill(signal) : 1);
357
276
  finish(ok({ stdout: markTruncated(out.text, out.droppedBytes), stderr: markTruncated(err.text, err.droppedBytes), exitCode }));
358
277
  })
@@ -363,9 +282,8 @@ export class RemoteSshExecutionEnv {
363
282
  });
364
283
  });
365
284
  }
366
- // ─────────────────────────────── streaming exec ───────────────────────────────
367
285
  async *execStream(command, options) {
368
- const ready = await this.ensureConnected(options?.signal); // [1165] 同 exec:首用自动建连(等待可中止)
286
+ const ready = await this.ensureConnected(options?.signal);
369
287
  if (!ready.ok)
370
288
  throw new RemoteExecutionError(ready.error.code === "aborted" ? "aborted" : "connect_failed", ready.error.message, ready.error);
371
289
  const conn = this.conn;
@@ -386,9 +304,7 @@ export class RemoteSshExecutionEnv {
386
304
  let streamRef;
387
305
  let onAbort;
388
306
  let paused = false;
389
- let done = false; // set in teardown; a conn.exec callback that fires AFTER the consumer cancelled must close its late stream
390
- // [R78]#1: the SSH transport dying mid-stream is a typed, retryable condition — map to "transport_lost"
391
- // (in the upper layer's retry allowlist) instead of a silent finish or "unknown".
307
+ let done = false;
392
308
  const onConnClose = () => {
393
309
  if (!finished) {
394
310
  failure = failure ?? new RemoteExecutionError("transport_lost", "ssh transport lost mid-stream (connection closed) — reconnect + retry per idempotency");
@@ -398,8 +314,6 @@ export class RemoteSshExecutionEnv {
398
314
  };
399
315
  conn.once("close", onConnClose);
400
316
  const teardown = () => {
401
- // runs on EVERY exit path (normal end, idle-timeout/failure throw, AND consumer early-break via the generator's
402
- // .return()): drop the abort listener (council #5) + close the channel (council #2 idle-timeout leak).
403
317
  done = true;
404
318
  conn.removeListener("close", onConnClose);
405
319
  if (onAbort) {
@@ -410,7 +324,6 @@ export class RemoteSshExecutionEnv {
410
324
  streamRef?.close();
411
325
  }
412
326
  catch {
413
- /* noop */
414
327
  }
415
328
  };
416
329
  const full = this.withCwdEnv(command, options?.cwd, options?.env);
@@ -424,10 +337,9 @@ export class RemoteSshExecutionEnv {
424
337
  streamRef = stream;
425
338
  if (done) {
426
339
  try {
427
- stream.close(); // consumer already cancelled (.return()/throw) before this callback ran → close the late channel (council #3)
340
+ stream.close();
428
341
  }
429
342
  catch {
430
- /* noop */
431
343
  }
432
344
  return;
433
345
  }
@@ -446,7 +358,7 @@ export class RemoteSshExecutionEnv {
446
358
  lastChunk = Date.now();
447
359
  queue.push(chunk);
448
360
  if (queue.length >= 512 && !paused) {
449
- paused = true; // backpressure: stop a fast producer from growing the queue unbounded (council design #1)
361
+ paused = true;
450
362
  stream.pause();
451
363
  }
452
364
  signalReady();
@@ -455,11 +367,8 @@ export class RemoteSshExecutionEnv {
455
367
  .on("data", (d) => enqueue({ type: "stdout", data: d.toString() }))
456
368
  .on("close", (code, signal) => {
457
369
  if (code == null && !signal && !failure) {
458
- // channel died without an exit status (transport drop) — typed retryable, not a fake exit 0 ([R78]#1)
459
370
  failure = new RemoteExecutionError("transport_lost", "ssh channel closed without exit status (transport lost)");
460
371
  }
461
- // code==null WITH a signal = the remote command was killed by that signal — conventional 128+signo,
462
- // NOT a fake success 0 ([R78]#2; was silently mapped to exitCode 0 before this fix).
463
372
  exitCode = code ?? (signal ? exitCodeForSignalKill(signal) : 0);
464
373
  finished = true;
465
374
  signalReady();
@@ -478,9 +387,7 @@ export class RemoteSshExecutionEnv {
478
387
  throw failure;
479
388
  if (finished)
480
389
  break;
481
- // idle/liveness bound: no chunk within readTimeoutMs → typed timeout (unreachable host)
482
390
  const waitMs = idleMs ? Math.max(1, idleMs - (Date.now() - lastChunk)) : 30_000;
483
- // [4947] A-002.11 sibling: clear the losing timer — wake() firing first used to leave it armed.
484
391
  let waitTimer;
485
392
  try {
486
393
  await new Promise((r) => {
@@ -503,11 +410,8 @@ export class RemoteSshExecutionEnv {
503
410
  teardown();
504
411
  }
505
412
  }
506
- // ─────────────────────────────── FileSystem (sftp + shell hybrid) ───────────────────────────────
507
413
  async sftp(abortSignal) {
508
- const ready = await this.ensureConnected(abortSignal); // [1165] fs 面同享懒连接(scratchpad mkdir 是 POC 实红点)
509
- // 四轮复审:取消跨 sftp 边界保码——aborted 直铸 FileError("aborted")(fileError() 对 FileError 原样
510
- // 放行),不得降级成 "unknown" 触发上游误重试。
414
+ const ready = await this.ensureConnected(abortSignal);
511
415
  if (!ready.ok)
512
416
  return Promise.reject(ready.error.code === "aborted" ? new FileError("aborted", ready.error.message) : new Error(ready.error.message));
513
417
  if (!this.conn)
@@ -553,14 +457,9 @@ export class RemoteSshExecutionEnv {
553
457
  }
554
458
  async writeFile(p, content, abortSignal) {
555
459
  const abs = this.resolve(p);
556
- // sftp.writeFile does NOT create parent dirs → mkdir -p the parent first (parity with E2B's auto-create).
557
460
  const dir = path.posix.dirname(abs);
558
461
  if (dir && dir !== "." && dir !== "/") {
559
462
  const mk = await this.exec(`mkdir -p -- ${shellQuote(dir)}`, { abortSignal });
560
- // 🔴 `!mk.ok` 也必须 fail-closed(2026-07-25 补):原来只看 `exitCode !== 0`,于是 **exec 本身失败**
561
- // (设备/连接在这一拍掉线 ⇒ `transport_lost` / `shell_unavailable`)被整条吞掉,后面照样推载荷,
562
- // 而整个 `writeFile` 返回 **ok:true** —— 调用方看到的成功与真实故障点完全脱节。同族的
563
- // local-docker / k8s 用的正是 `if (!mk.ok) return`,这两条腿是漏的。
564
463
  if (!mk.ok)
565
464
  return { ok: false, error: fileErrorFromExec(mk.error, dir) };
566
465
  if (mk.value.exitCode !== 0)
@@ -609,7 +508,7 @@ export class RemoteSshExecutionEnv {
609
508
  const sftp = await this.sftp(abortSignal);
610
509
  const entries = await new Promise((resolve, reject) => sftp.readdir(abs, (err, list) => (err ? reject(err) : resolve(list))));
611
510
  return ok(entries.map((e) => {
612
- const a = e.attrs; // readdir gives raw Attributes (no isDirectory() helper, unlike lstat's Stats) → derive from mode bits
511
+ const a = e.attrs;
613
512
  return { name: e.filename, path: path.posix.join(abs, e.filename), kind: kindFromMode(a.mode), size: a.size ?? 0, mtimeMs: (a.mtime ?? 0) * 1000 };
614
513
  }));
615
514
  }
@@ -617,13 +516,12 @@ export class RemoteSshExecutionEnv {
617
516
  return { ok: false, error: this.fileError(e, abs) };
618
517
  }
619
518
  }
620
- // ── design/158 B6:shell-out 文件面委托共享基座(posix-shell-fs.ts;fileInfo/listDir/exists 走 SFTP 原生,不并)──
621
519
  posixFsInst;
622
520
  get posixFs() {
623
521
  return (this.posixFsInst ??= createPosixShellFs({
624
522
  run: (cmd, abortSignal) => this.exec(cmd, { abortSignal }),
625
523
  resolve: (p) => this.resolve(p),
626
- tempDir: () => null, // R5:ssh 无 workspace 概念,临时件落系统 temp(mktemp -t)
524
+ tempDir: () => null,
627
525
  }));
628
526
  }
629
527
  async readLink(p, abortSignal) {
@@ -652,7 +550,6 @@ export class RemoteSshExecutionEnv {
652
550
  async createTempFile(options) {
653
551
  return this.posixFs.createTempFile(options);
654
552
  }
655
- // ─────────────────────────────── helpers ───────────────────────────────
656
553
  withCwdEnv(command, cwd, env) {
657
554
  const parts = [];
658
555
  const dir = cwd ?? (this.cwd !== "." ? this.cwd : undefined);
@@ -667,7 +564,7 @@ export class RemoteSshExecutionEnv {
667
564
  connErr(e) {
668
565
  const msg = e.message || String(e);
669
566
  if (e.level === "client-authentication" || /authentication|all configured authentication methods failed/i.test(msg)) {
670
- return new RemoteExecutionError("auth_failed", `ssh auth failed: ${msg}`, e); // permanent — not retried
567
+ return new RemoteExecutionError("auth_failed", `ssh auth failed: ${msg}`, e);
671
568
  }
672
569
  if (e.level === "client-timeout" || /timed out|timeout/i.test(msg))
673
570
  return new RemoteExecutionError("timeout", `ssh connect timed out: ${msg}`, e);
@@ -689,11 +586,6 @@ export class RemoteSshExecutionEnv {
689
586
  return new FileError("unknown", msg, p);
690
587
  }
691
588
  }
692
- /**
693
- * `ExecutionEnvFactory` for the SSH backend — one fresh (unconnected) env per task against the same host
694
- * (lazy: connects on first use). Config (host/user/key) is control-plane-supplied; the Runner owns the
695
- * lifetime and calls `destroy()` (= disconnect) on task end.
696
- */
697
589
  export function sshExecutionEnvFactory(config) {
698
590
  return (_ctx) => new RemoteSshExecutionEnv(config);
699
591
  }
@@ -1,46 +1,26 @@
1
- /** The remote lanes that get the in-sandbox scratchpad convention. adb (no standard /tmp) and host
2
- * (worker-local scratchpad already exists) are deliberately absent. */
3
1
  const REMOTE_SCRATCHPAD_LANES = new Set(["e2b", "k8s", "local-docker", "ssh"]);
4
2
  export function isRemoteScratchpadLane(provider) {
5
3
  return provider !== undefined && REMOTE_SCRATCHPAD_LANES.has(provider);
6
4
  }
7
- /** Charset gate for embedding the sessionId in a shell command. Dots allowed (unlike the host-lane segment
8
- * sanitizer, which REWRITES): here an id either passes verbatim or the feature stays off. "/" can't pass, so a
9
- * multi-segment escape can't — but the two SINGLE-segment escapes `.` (= the shared scratchpad root) and `..`
10
- * (= /tmp itself) DO match the charset and are rejected explicitly below (codex HIGH: an opaque caller-chosen
11
- * sessionId of exactly "."/".." would alias another session's root on a shared ssh host). */
12
5
  const SESSION_ID_RE = /^[A-Za-z0-9._-]+$/;
13
- /** THE remote scratchpad path convention: `/tmp/scratchpad/<sessionId>` (raw id — the sandbox is already
14
- * session/task-scoped tenancy, no hash segment needed). Returns undefined for an absent/unsafe id — the
15
- * single validity rule shared by the factory decorator AND the envFacts advertisement. */
16
6
  export function remoteScratchpadDirFor(sessionId) {
17
7
  if (!sessionId || sessionId === "." || sessionId === ".." || !SESSION_ID_RE.test(sessionId))
18
8
  return undefined;
19
9
  return `/tmp/scratchpad/${sessionId}`;
20
10
  }
21
- /**
22
- * Decorate a factory so each minted env lazily `mkdir -p`s its session scratchpad before its FIRST exec.
23
- * The env is mutated IN PLACE (rebind `env.exec`, evictLspOnDestroy precedent) — a copy would drop the
24
- * adapter's prototype methods and identity (registries key on the instance).
25
- */
26
11
  export function withRemoteScratchpad(factory, logger) {
27
12
  return async (ctx) => {
28
13
  const env = await factory(ctx);
29
14
  const dir = remoteScratchpadDirFor(ctx.sessionId);
30
15
  if (dir === undefined) {
31
- // absent id (adhoc no-session spawn) = silently no scratchpad; a NON-empty id failing the charset gate
32
- // is a wiring anomaly worth a warn (our ids are uuids — this should never fire outside a bug).
33
16
  if (ctx.sessionId)
34
17
  logger?.warn?.("remote_scratchpad_skipped_unsafe_session_id", { sessionId: ctx.sessionId });
35
18
  return env;
36
19
  }
37
20
  const e = env;
38
21
  if (typeof e.exec !== "function")
39
- return env; // probe-guard: no exec face, nothing to ride
22
+ return env;
40
23
  const origExec = e.exec.bind(e);
41
- // Single-flight via a cached PROMISE (not a boolean): concurrent first commands await the SAME mkdir.
42
- // The promise never rejects (failure is swallowed into the warn) and stays cached once settled — mkdir
43
- // runs AT MOST once per env, success or not (best-effort posture, see file header).
44
24
  let ensured;
45
25
  const ensure = () => (ensured ??= (async () => {
46
26
  try {
@@ -57,17 +37,11 @@ export function withRemoteScratchpad(factory, logger) {
57
37
  }
58
38
  })());
59
39
  e.exec = async (command, options) => {
60
- // Pre-aborted call must NOT trigger the mkdir prelude: on a lazy-VM lane the adapter's own exec guard
61
- // refuses BEFORE provisioning, but our optionless mkdir would bypass that guard and force-boot a billable
62
- // VM just to abort (codex M2). Mid-mkdir aborts stay coarse (the command is trivial) — accepted.
63
40
  if (options?.abortSignal?.aborted)
64
41
  return origExec(command, options);
65
42
  await ensure();
66
43
  return origExec(command, options);
67
44
  };
68
- // Background leg (codex M1): core's background path calls `spawnBackground`, NOT `exec` — a task whose FIRST
69
- // command is a bg spawn must ride the SAME single-flight mkdir or its scratchpad write fails. Duck-typed (the
70
- // capability is optional per hasBackgroundShell); BackgroundSpawnOptions has no abortSignal, so no pre-abort leg.
71
45
  const bgHost = e;
72
46
  if (typeof bgHost.spawnBackground === "function") {
73
47
  const origSpawn = bgHost.spawnBackground.bind(bgHost);
@@ -76,16 +50,12 @@ export function withRemoteScratchpad(factory, logger) {
76
50
  return origSpawn(command, options);
77
51
  };
78
52
  }
79
- // PAIR-REVIEW F-5:canonicalPath 腿**显式**接 ensure——core 1.351 的 prepare 期 scratchpad 实体化/
80
- // additionalRoots canonicalize 走 env.canonicalPath;local-docker 的实现私径直调 docker(绕 exec 包装)
81
- // → 目录未建 not_found → 围栏不 admit(广告了但 Write 恒拒,[1339] 原病残留)。e2b/ssh/k8s 此前只是
82
- // **恰好**内部走 this.exec 被顺带救活(巧合承重)——此腿把六 adapter 拉到同一显式契约。
83
53
  const cpHost = e;
84
54
  if (typeof cpHost.canonicalPath === "function") {
85
55
  const origCanonical = cpHost.canonicalPath.bind(cpHost);
86
56
  cpHost.canonicalPath = async (p, abortSignal) => {
87
57
  if (abortSignal?.aborted)
88
- return origCanonical(p, abortSignal); // 同 exec 腿:预中止不强启 VM
58
+ return origCanonical(p, abortSignal);
89
59
  await ensure();
90
60
  return origCanonical(p, abortSignal);
91
61
  };
@@ -1,20 +1,4 @@
1
- /**
2
- * D5 pipe-destroy grace (parity with core's vendored `EXEC_FORCE_SETTLE_GRACE_MS`, harness/env/nodejs.js):
3
- * after a `kill()` on an abort/timeout, the child's `close` may NEVER fire — a re-parented grandchild that
4
- * inherited the stdout/stderr pipe (or a D-state process) can hold those FDs open indefinitely. Our buffered
5
- * exec already SETTLES the promise synchronously on abort/timeout (no hang), but the dangling pipe FDs leak.
6
- * So after a kill we arm a short grace timer; if `close` hasn't fired by then we `destroy()` the pipes to
7
- * force the FDs released. Core does NOT export this const (it's a vendor-local value), so service defines its
8
- * own — a plain number, not a security/behavioural boundary.
9
- */
10
1
  export const EXEC_FORCE_SETTLE_GRACE_MS = 3000;
11
- /**
12
- * D5 helper: arm a one-shot grace timer that `destroy()`s {@link ChildProcess.stdout}/`stderr` if the child's
13
- * `close` never lands after a kill (a re-parented grandchild holding the pipe → dangling FD). Returns the
14
- * timer so the caller can {@link clearTimeout} it the moment a real `close` (or any settle) arrives — the timer
15
- * is `unref`'d so it can never keep the process alive on its own. This ONLY frees pipe FDs; it does NOT settle
16
- * any promise (the buffered exec already settled synchronously on abort/timeout — that path is unchanged).
17
- */
18
2
  export function armPipeDestroyGrace(child) {
19
3
  const t = setTimeout(() => {
20
4
  try {
@@ -22,18 +6,14 @@ export function armPipeDestroyGrace(child) {
22
6
  child.stderr?.destroy();
23
7
  }
24
8
  catch {
25
- /* best-effort pipe teardown — never throw from a timer */
26
9
  }
27
10
  }, EXEC_FORCE_SETTLE_GRACE_MS);
28
11
  t.unref?.();
29
12
  return t;
30
13
  }
31
- /** POSIX single-quote a string for safe interpolation into a remote shell command (the only metacharacter
32
- * inside '' is ' itself, closed/escaped/reopened). */
33
14
  export function shellQuote(s) {
34
15
  return `'${s.replace(/'/g, `'\\''`)}'`;
35
16
  }
36
- /** Derive FileInfo kind from POSIX st_mode bits (raw stat/readdir attrs carry no isDirectory() helper). */
37
17
  export function kindFromMode(mode) {
38
18
  const t = (mode ?? 0) & 0o170000;
39
19
  if (t === 0o040000)
@@ -42,13 +22,6 @@ export function kindFromMode(mode) {
42
22
  return "symlink";
43
23
  return "file";
44
24
  }
45
- // ── design/158 B4 交接小件:plugins 数值旋钮的 fail-loud ─────────────────────────────────────────────
46
- // HOST_BG_*/K8S_BG_*/E2B_* 是模块级、import 时读的常量,不走 ServiceConfig;此前手搓
47
- // `Math.max(min, Math.floor(Number(env ?? "")) || def)` 让非法字面量(`1mb`)静默落默认——与 config.ts
48
- // boolEnv 修掉的同族病。告警收本模块自带通道(不造 plugins→config 依赖边),main.ts 在 boot 诊断点
49
- // (logConfigDiagnostics 旁)排空——ESM import 先于 main 体执行,时序成立。
50
- // 语义逐字承原形:unset/"" 静默默认;显式 0 仍落默认(0 在这族旋钮上无合法语义,关功能走 *_ENABLED);
51
- // 合法值 floor+min 夹紧。唯一新增行为=非法字面量推一条警告。
52
25
  const NUM_ENV_WARNINGS = [];
53
26
  const NUM_ENV_SEEN = new Set();
54
27
  export function drainNumEnvWarnings() {
@@ -57,12 +30,8 @@ export function drainNumEnvWarnings() {
57
30
  export function numEnvOr(name, def, min) {
58
31
  const raw = process.env[name] ?? "";
59
32
  const n = Math.floor(Number(raw));
60
- // A-067 C1-1(F2a):判据必须是 isFinite,不是 isNaN——`"Infinity"`/`"1e999"` 穿过 NaN 检查后
61
- // `n || def` 取 Infinity、`Math.max(min, Infinity)` 恒 Infinity ⇒ 本函数独供的 17 个执行 lane
62
- // 旋钮里,E2B_SANDBOX_MAX_MS 的消费点 `Math.min(callerDeadline, cap)`(BL-31 防挂钳)被整条击穿。
63
- // 非有限=非法字面量同待遇:警告 + 落默认。
64
33
  if (raw !== "" && !Number.isFinite(n) && !NUM_ENV_SEEN.has(name)) {
65
- NUM_ENV_SEEN.add(name); // 首登记守卫(boolEnv 同款):懒/重复读不无界增长
34
+ NUM_ENV_SEEN.add(name);
66
35
  NUM_ENV_WARNINGS.push({ env: name, raw });
67
36
  }
68
37
  return Math.max(min, (Number.isFinite(n) ? n : 0) || def);
@@ -1,46 +1,35 @@
1
1
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
2
- /** Dual-dialect durable resume-at anchor map. See the file header for the dialect-delta ledger. */
3
2
  export class SqlResumeAnchorStore {
4
3
  db;
5
4
  constructor(db) {
6
5
  this.db = db;
7
6
  }
8
- /** Pick the dialect's SQL text. Both statements stay written out at the call site ON PURPOSE. */
9
7
  q(tidb, pg) {
10
8
  return this.db.dialect === "tidb" ? tidb : pg;
11
9
  }
12
- /** Record/overwrite the anchor for one settled message boundary (idempotent upsert on the (session_id,event_id) PK). */
13
10
  async put(sessionId, eventId, entryId, owner) {
14
11
  await this.db.query(this.q("INSERT INTO resume_anchor (session_id, event_id, entry_id, owner, created_at) VALUES (?,?,?,?,?) " +
15
12
  "ON DUPLICATE KEY UPDATE entry_id = VALUES(entry_id), owner = VALUES(owner)", "INSERT INTO resume_anchor (session_id, event_id, entry_id, owner, created_at) VALUES ($1,$2,$3,$4,$5) " +
16
13
  "ON CONFLICT (session_id, event_id) DO UPDATE SET entry_id = EXCLUDED.entry_id, owner = EXCLUDED.owner"), [sessionId, eventId, entryId, owner, new Date()]);
17
14
  }
18
- /** Resolve a shell-supplied eventId → the persisted SessionTreeEntry.id; undefined = unknown anchor. */
19
15
  async resolve(sessionId, eventId, owner) {
20
16
  const { rows } = await this.db.query(this.q("SELECT entry_id FROM resume_anchor WHERE session_id = ? AND event_id = ? AND owner <=> ?", "SELECT entry_id FROM resume_anchor WHERE session_id = $1 AND event_id = $2 AND owner IS NOT DISTINCT FROM $3"), [sessionId, eventId, owner]);
21
17
  return rows[0] ? String(rows[0].entry_id) : undefined;
22
18
  }
23
- /** 2c session-sync: ALL anchors for a session (every eventId→entryId + its owner), UNFILTERED — a cross-backend
24
- * session EXPORT bundles the whole anchor map so the importer can RE-KEY owner to the target principal (§9). The
25
- * route already proved session ownership (the export gate), so no per-row owner filter here. owner carried verbatim. */
26
19
  async listBySession(sessionId) {
27
20
  const { rows } = await this.db.query(this.q("SELECT event_id, entry_id, owner FROM resume_anchor WHERE session_id = ?", "SELECT event_id, entry_id, owner FROM resume_anchor WHERE session_id = $1"), [sessionId]);
28
21
  return rows.map((r) => ({ eventId: String(r.event_id), entryId: String(r.entry_id), owner: r.owner == null ? null : String(r.owner) }));
29
22
  }
30
- /** E21 — purge ALL anchors for a session, scoped by session_id ALONE (the route proved ownership; a per-row owner
31
- * guard would orphan rows whose per-run principal diverged from the session owner). Idempotent (affected count). */
32
23
  async deleteBySession(sessionId) {
33
24
  const res = await this.db.query(this.q("DELETE FROM resume_anchor WHERE session_id = ?", "DELETE FROM resume_anchor WHERE session_id = $1"), [sessionId]);
34
25
  return res.affected;
35
26
  }
36
27
  }
37
- /** MySQL-protocol (TiDB) binding — historical class name + ctor shape preserved. */
38
28
  export class TiDBResumeAnchorStore extends SqlResumeAnchorStore {
39
29
  constructor(pool) {
40
30
  super(mysqlDriver(pool));
41
31
  }
42
32
  }
43
- /** PostgreSQL binding — historical class name + ctor shape preserved. */
44
33
  export class PgResumeAnchorStore extends SqlResumeAnchorStore {
45
34
  constructor(pool) {
46
35
  super(pgDriver(pool));