@sema-agent/server 7.138.0 → 7.140.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 (332) hide show
  1. package/MIGRATION.md +457 -7
  2. package/README.md +7 -4
  3. package/README.zh-CN.md +6 -4
  4. package/USAGE.md +91 -41
  5. package/deploy/sema-up/README.md +1 -1
  6. package/deploy/sema-up/lib/lanes.json +3 -2
  7. package/dist/adoption/runner.js +1 -1
  8. package/dist/adoption/sql.js +1 -1
  9. package/dist/adoption/wire.d.ts +3 -3
  10. package/dist/approval-card.d.ts +26 -26
  11. package/dist/bench/s1/arms.d.ts +1 -1
  12. package/dist/bench/s1/arms.js +1 -3
  13. package/dist/bench/s1/live-deps.js +3 -2
  14. package/dist/bench/s1/row.d.ts +5 -2
  15. package/dist/bench/s1/row.js +2 -22
  16. package/dist/bench/s1/run-firm.js +2 -2
  17. package/dist/boot/adoption.js +1 -1
  18. package/dist/boot/device-lane.d.ts +8 -3
  19. package/dist/boot/device-lane.js +7 -2
  20. package/dist/boot/execution-env.d.ts +4 -2
  21. package/dist/boot/execution-env.js +28 -34
  22. package/dist/boot/execution-lane.d.ts +9 -1
  23. package/dist/boot/execution-lane.js +6 -1
  24. package/dist/boot/governance-seams.d.ts +3 -2
  25. package/dist/boot/governance-seams.js +9 -11
  26. package/dist/boot/memory-boundary.d.ts +14 -51
  27. package/dist/boot/memory-boundary.js +10 -20
  28. package/dist/boot/memory-consolidation.d.ts +15 -11
  29. package/dist/boot/memory-consolidation.js +25 -14
  30. package/dist/boot/resolve-spec.d.ts +4 -6
  31. package/dist/boot/resolve-spec.js +10 -13
  32. package/dist/boot/retention-lane.d.ts +5 -5
  33. package/dist/boot/retention-lane.js +0 -6
  34. package/dist/boot/runner-deps.d.ts +6 -4
  35. package/dist/boot/runner-deps.js +3 -1
  36. package/dist/boot/session-faces.d.ts +0 -3
  37. package/dist/boot/session-faces.js +15 -8
  38. package/dist/boot/stage-02-web-admin-pull.js +2 -2
  39. package/dist/boot/stage-03-store-backend.d.ts +2 -6
  40. package/dist/boot/stage-03-store-backend.js +2 -3
  41. package/dist/boot/stage-04-stores.d.ts +2 -6
  42. package/dist/boot/stage-04-stores.js +5 -5
  43. package/dist/boot/stage-05-execution-env.d.ts +2 -6
  44. package/dist/boot/stage-05-execution-env.js +2 -2
  45. package/dist/boot/stage-06-runners.d.ts +2 -6
  46. package/dist/boot/stage-06-runners.js +2 -3
  47. package/dist/boot/stage-07-capability-layer.d.ts +2 -6
  48. package/dist/boot/stage-08-reapers.d.ts +2 -6
  49. package/dist/boot/stage-09-leader.d.ts +2 -6
  50. package/dist/boot/stage-09-leader.js +1 -2
  51. package/dist/boot/stage-10-http-server.d.ts +2 -6
  52. package/dist/boot/stage-10-http-server.js +7 -6
  53. package/dist/boot/stores.d.ts +3 -6
  54. package/dist/boot/stores.js +27 -40
  55. package/dist/boot/task-list-lane.d.ts +1 -1
  56. package/dist/boot/task-list-lane.js +1 -1
  57. package/dist/boot/web-admin.js +37 -11
  58. package/dist/capabilities/execution-lane.d.ts +2 -2
  59. package/dist/capabilities/mcp-probe.js +1 -1
  60. package/dist/capabilities/scenarios.d.ts +1 -1
  61. package/dist/capability-facts.d.ts +15 -7
  62. package/dist/capability-facts.js +8 -3
  63. package/dist/config-catalog.js +9 -8
  64. package/dist/config-invariants.d.ts +23 -3
  65. package/dist/config-invariants.js +42 -0
  66. package/dist/config-provider.d.ts +33 -37
  67. package/dist/config-provider.js +51 -30
  68. package/dist/config-types.d.ts +21 -7
  69. package/dist/config.d.ts +33 -0
  70. package/dist/config.js +46 -16
  71. package/dist/delegated-session-owner.d.ts +4 -3
  72. package/dist/delegated-session-owner.js +1 -1
  73. package/dist/deployment-shape.d.ts +10 -9
  74. package/dist/deployment-shape.js +4 -4
  75. package/dist/device-enrollment.d.ts +41 -2
  76. package/dist/device-enrollment.js +27 -12
  77. package/dist/device-store.d.ts +58 -85
  78. package/dist/device-store.js +19 -49
  79. package/dist/device-ws-hub.js +2 -1
  80. package/dist/env-name-allowlist-knobs.d.ts +5 -4
  81. package/dist/exec-cut.d.ts +60 -0
  82. package/dist/exec-cut.js +36 -0
  83. package/dist/execution-lane-caps.d.ts +115 -105
  84. package/dist/execution-lane-caps.js +76 -82
  85. package/dist/execution-placement.d.ts +259 -0
  86. package/dist/execution-placement.js +199 -0
  87. package/dist/field-conflict.d.ts +3 -2
  88. package/dist/field-conflict.js +1 -0
  89. package/dist/hosted-posture.d.ts +2 -2
  90. package/dist/http/admission.js +8 -19
  91. package/dist/http/checkpoint-ctx.d.ts +21 -12
  92. package/dist/http/checkpoint-ctx.js +4 -2
  93. package/dist/http/claimed-run.d.ts +1 -1
  94. package/dist/http/durable-run-leg.js +9 -5
  95. package/dist/http/execution-context.d.ts +218 -0
  96. package/dist/http/execution-context.js +330 -0
  97. package/dist/http/memory-compliance-failure-reply.d.ts +3 -1
  98. package/dist/http/memory-compliance-failure-reply.js +5 -0
  99. package/dist/http/memory-layer-reply.d.ts +30 -0
  100. package/dist/http/memory-layer-reply.js +24 -0
  101. package/dist/http/resume-legs.js +63 -36
  102. package/dist/http/retired-service-deps.d.ts +1 -1
  103. package/dist/http/retired-service-deps.js +1 -1
  104. package/dist/http/route-ctx.d.ts +14 -8
  105. package/dist/http/routes/adoption.js +1 -1
  106. package/dist/http/routes/background.d.ts +29 -1
  107. package/dist/http/routes/background.js +29 -2
  108. package/dist/http/routes/devices.d.ts +13 -19
  109. package/dist/http/routes/devices.js +21 -26
  110. package/dist/http/routes/memory-bundle.js +3 -0
  111. package/dist/http/routes/memory-compliance.js +8 -1
  112. package/dist/http/routes/memory-consolidation.js +12 -1
  113. package/dist/http/routes/memory-origin.js +14 -2
  114. package/dist/http/routes/memory-policy.js +5 -0
  115. package/dist/http/routes/session-sync.js +110 -15
  116. package/dist/http/routes/sessions.d.ts +6 -0
  117. package/dist/http/routes/sessions.js +143 -22
  118. package/dist/http/routes/side-query.d.ts +12 -4
  119. package/dist/http/routes/side-query.js +4 -5
  120. package/dist/http/routes/tasks.js +41 -33
  121. package/dist/http/server.d.ts +40 -44
  122. package/dist/http/server.js +13 -2
  123. package/dist/http/session-inherit-env.d.ts +3 -4
  124. package/dist/http/session-inherit-env.js +1 -1
  125. package/dist/http/wire-types.d.ts +8 -12
  126. package/dist/identity-source.d.ts +1 -1
  127. package/dist/identity-source.js +1 -1
  128. package/dist/index.d.ts +1 -0
  129. package/dist/lane-home-probe.d.ts +68 -42
  130. package/dist/lane-home-probe.js +53 -30
  131. package/dist/leader/wire.js +5 -4
  132. package/dist/memory-bundle-engine.d.ts +16 -9
  133. package/dist/memory-bundle-engine.js +29 -6
  134. package/dist/memory-operator-faces.d.ts +85 -30
  135. package/dist/memory-operator-faces.js +136 -30
  136. package/dist/memory-project-layer.d.ts +3 -21
  137. package/dist/memory-project-layer.js +11 -8
  138. package/dist/memory-scope-stores.d.ts +73 -0
  139. package/dist/memory-scope-stores.js +86 -0
  140. package/dist/memory-scope.d.ts +29 -5
  141. package/dist/memory-scope.js +16 -4
  142. package/dist/memory-sync-client.d.ts +70 -18
  143. package/dist/memory-sync-client.js +215 -117
  144. package/dist/memory-sync.d.ts +1 -1
  145. package/dist/memory-sync.js +3 -2
  146. package/dist/observability/cost-taxonomy.d.ts +2 -2
  147. package/dist/observability/fail-open.d.ts +12 -0
  148. package/dist/observability/fail-open.js +12 -0
  149. package/dist/observability/run-terminal.js +0 -1
  150. package/dist/orchestration/workflow-notify-journal.js +4 -4
  151. package/dist/org-memory-admission.d.ts +4 -3
  152. package/dist/org-memory-admission.js +6 -6
  153. package/dist/parked-decide.d.ts +34 -9
  154. package/dist/parked-decide.js +12 -19
  155. package/dist/parked-revive-surface.d.ts +12 -15
  156. package/dist/parked-revive-surface.js +3 -6
  157. package/dist/peer-seat.d.ts +13 -7
  158. package/dist/peer-seat.js +37 -3
  159. package/dist/plugins/adoption-log-sql.js +4 -4
  160. package/dist/plugins/approval-ask-store-file.d.ts +7 -0
  161. package/dist/plugins/approval-ask-store-file.js +11 -0
  162. package/dist/plugins/approval-ask-store-memory.d.ts +6 -0
  163. package/dist/plugins/approval-ask-store-memory.js +12 -0
  164. package/dist/plugins/approval-ask-store-sql.d.ts +22 -5
  165. package/dist/plugins/approval-ask-store-sql.js +44 -26
  166. package/dist/plugins/approval-nonce-store.js +4 -4
  167. package/dist/plugins/blob-backend.d.ts +4 -4
  168. package/dist/plugins/blob-backend.js +7 -7
  169. package/dist/plugins/breaker-state-sql.js +2 -2
  170. package/dist/plugins/caching-session-store.d.ts +6 -2
  171. package/dist/plugins/caching-session-store.js +9 -3
  172. package/dist/plugins/checkpoint-index-columns.js +2 -2
  173. package/dist/plugins/checkpoint-store-sql.d.ts +6 -5
  174. package/dist/plugins/checkpoint-store-sql.js +21 -23
  175. package/dist/plugins/device-store-sql.d.ts +25 -51
  176. package/dist/plugins/device-store-sql.js +68 -122
  177. package/dist/plugins/ensure-index.d.ts +1 -2
  178. package/dist/plugins/execution-placement-sql.d.ts +151 -0
  179. package/dist/plugins/execution-placement-sql.js +223 -0
  180. package/dist/plugins/file-history-store-sql.d.ts +9 -8
  181. package/dist/plugins/file-history-store-sql.js +9 -9
  182. package/dist/plugins/file-run-store.d.ts +6 -1
  183. package/dist/plugins/file-run-store.js +4 -0
  184. package/dist/plugins/fork-routing-session-store.d.ts +6 -0
  185. package/dist/plugins/fork-routing-session-store.js +16 -0
  186. package/dist/plugins/image-bake-store-sql.js +9 -9
  187. package/dist/plugins/image-index-sql.js +11 -11
  188. package/dist/plugins/leader-run-store-sql.js +4 -4
  189. package/dist/plugins/local-session-store.d.ts +149 -50
  190. package/dist/plugins/local-session-store.js +382 -107
  191. package/dist/plugins/mailbox-store-sql.d.ts +2 -1
  192. package/dist/plugins/mailbox-store-sql.js +4 -4
  193. package/dist/plugins/memory-engine-tidb.js +5 -5
  194. package/dist/plugins/memory-key-guards.d.ts +4 -0
  195. package/dist/plugins/memory-key-guards.js +1 -0
  196. package/dist/plugins/memory-optout-grant-store-sql.js +1 -1
  197. package/dist/plugins/memory-run-store.d.ts +1 -1
  198. package/dist/plugins/memory-sync-store-tidb.js +3 -3
  199. package/dist/plugins/one-time-migrations.d.ts +117 -8
  200. package/dist/plugins/one-time-migrations.js +408 -38
  201. package/dist/plugins/org-delivery-quota-store.js +2 -2
  202. package/dist/plugins/outcome-ledger-sql.d.ts +2 -2
  203. package/dist/plugins/outcome-ledger-sql.js +2 -2
  204. package/dist/plugins/permission-rule-store-sql.d.ts +17 -19
  205. package/dist/plugins/permission-rule-store-sql.js +29 -32
  206. package/dist/plugins/pg-pool.d.ts +7 -3
  207. package/dist/plugins/pg-session-storage.d.ts +31 -20
  208. package/dist/plugins/pg-session-storage.js +120 -52
  209. package/dist/plugins/remote-env-adb.d.ts +18 -19
  210. package/dist/plugins/remote-env-adb.js +91 -66
  211. package/dist/plugins/remote-env-device.d.ts +23 -31
  212. package/dist/plugins/remote-env-device.js +19 -17
  213. package/dist/plugins/remote-env-e2b.d.ts +27 -21
  214. package/dist/plugins/remote-env-e2b.js +91 -45
  215. package/dist/plugins/remote-env-file-error.d.ts +3 -1
  216. package/dist/plugins/remote-env-file-error.js +9 -3
  217. package/dist/plugins/remote-env-host.d.ts +31 -34
  218. package/dist/plugins/remote-env-host.js +91 -72
  219. package/dist/plugins/remote-env-k8s.d.ts +13 -29
  220. package/dist/plugins/remote-env-k8s.js +63 -55
  221. package/dist/plugins/remote-env-local-docker.d.ts +24 -12
  222. package/dist/plugins/remote-env-local-docker.js +49 -61
  223. package/dist/plugins/remote-env-ssh.d.ts +11 -19
  224. package/dist/plugins/remote-env-ssh.js +74 -57
  225. package/dist/plugins/remote-shell.d.ts +100 -3
  226. package/dist/plugins/remote-shell.js +166 -2
  227. package/dist/plugins/resume-anchor-store-sql.js +2 -2
  228. package/dist/plugins/retention-lane-store-sql.d.ts +1 -1
  229. package/dist/plugins/retention-lane-store-sql.js +2 -2
  230. package/dist/plugins/retention-store-sql.d.ts +10 -7
  231. package/dist/plugins/retention-store-sql.js +13 -7
  232. package/dist/plugins/run-store-sql.d.ts +13 -3
  233. package/dist/plugins/run-store-sql.js +20 -15
  234. package/dist/plugins/schema-consistency.d.ts +21 -11
  235. package/dist/plugins/schema-consistency.js +15 -9
  236. package/dist/plugins/schema-drift-sentence.d.ts +27 -0
  237. package/dist/plugins/schema-drift-sentence.js +38 -0
  238. package/dist/plugins/schema-ensure-legs.d.ts +2 -3
  239. package/dist/plugins/schema-ensure-legs.js +2 -2
  240. package/dist/plugins/schema-live-columns.d.ts +1 -1
  241. package/dist/plugins/session-capture-record-store-sql.js +1 -1
  242. package/dist/plugins/session-fork.d.ts +54 -0
  243. package/dist/plugins/session-fork.js +38 -1
  244. package/dist/plugins/session-row-sql.d.ts +22 -0
  245. package/dist/plugins/session-row-sql.js +81 -0
  246. package/dist/plugins/session-rules-column.d.ts +1 -1
  247. package/dist/plugins/session-rules-column.js +1 -1
  248. package/dist/plugins/session-store.d.ts +1 -1
  249. package/dist/plugins/session-workspace-cell.d.ts +1 -1
  250. package/dist/plugins/session-workspace-cell.js +1 -1
  251. package/dist/plugins/shared-memory-store-sql.js +2 -2
  252. package/dist/plugins/sql-driver.d.ts +10 -5
  253. package/dist/plugins/sql-driver.js +6 -6
  254. package/dist/plugins/sql-errors.js +1 -1
  255. package/dist/plugins/store-backend.d.ts +5 -7
  256. package/dist/plugins/store-backend.js +23 -11
  257. package/dist/plugins/store-contracts.d.ts +25 -5
  258. package/dist/plugins/task-list-store-sql.js +11 -11
  259. package/dist/plugins/tidb-pool.d.ts +12 -7
  260. package/dist/plugins/tidb-pool.js +13 -7
  261. package/dist/plugins/tidb-session-storage.js +4 -4
  262. package/dist/plugins/tidb-session-store.d.ts +57 -26
  263. package/dist/plugins/tidb-session-store.js +127 -59
  264. package/dist/plugins/tool-result-store-sql.d.ts +5 -4
  265. package/dist/plugins/tool-result-store-sql.js +21 -13
  266. package/dist/plugins/user-input-ledger-sql.js +2 -2
  267. package/dist/plugins/workflow-journal-store-sql.js +5 -5
  268. package/dist/plugins/workflow-run-store-sql.js +19 -19
  269. package/dist/plugins/write-behind-counter.d.ts +2 -3
  270. package/dist/plugins/write-behind-counter.js +5 -5
  271. package/dist/project-memory.d.ts +9 -15
  272. package/dist/project-memory.js +5 -4
  273. package/dist/request-key-closure.js +1 -1
  274. package/dist/request-value-closure.d.ts +6 -7
  275. package/dist/request-value-closure.js +1 -3
  276. package/dist/resource-suspend.d.ts +14 -12
  277. package/dist/resource-suspend.js +1 -1
  278. package/dist/resource-window.d.ts +4 -3
  279. package/dist/run-local.d.ts +6 -0
  280. package/dist/run-local.js +34 -35
  281. package/dist/runs.d.ts +10 -5
  282. package/dist/runs.js +52 -66
  283. package/dist/security.d.ts +38 -12
  284. package/dist/security.js +8 -2
  285. package/dist/server-secret-env.d.ts +9 -5
  286. package/dist/session-children-retire.d.ts +27 -9
  287. package/dist/session-children-retire.js +9 -5
  288. package/dist/session-incarnation.d.ts +20 -10
  289. package/dist/session-incarnation.js +5 -3
  290. package/dist/session-policy-wire.d.ts +1 -1
  291. package/dist/session-row.d.ts +114 -5
  292. package/dist/session-row.js +55 -5
  293. package/dist/session-titler.d.ts +2 -2
  294. package/dist/sql-engine-posture.d.ts +1 -1
  295. package/dist/sql-engine-posture.js +1 -1
  296. package/dist/{plugins → stores-sql}/background-agent-store-sql.d.ts +2 -2
  297. package/dist/{plugins → stores-sql}/background-agent-store-sql.js +8 -8
  298. package/dist/{plugins → stores-sql}/session-policy-store-sql.d.ts +7 -5
  299. package/dist/{plugins → stores-sql}/session-policy-store-sql.js +12 -12
  300. package/dist/{plugins → stores-sql}/usage-window-store-sql.d.ts +1 -1
  301. package/dist/{plugins → stores-sql}/usage-window-store-sql.js +3 -3
  302. package/dist/task-a2a.d.ts +1 -1
  303. package/dist/task-a2a.js +5 -6
  304. package/dist/task-cwd.d.ts +6 -1
  305. package/dist/task-mcp.d.ts +22 -9
  306. package/dist/task-mcp.js +17 -9
  307. package/dist/terminal.d.ts +27 -0
  308. package/dist/terminal.js +6 -0
  309. package/dist/tool-approval.d.ts +6 -0
  310. package/dist/tool-approval.js +8 -1
  311. package/dist/trace/project.d.ts +1 -0
  312. package/dist/trace/project.js +1 -0
  313. package/dist/trace/redact.d.ts +3 -1
  314. package/dist/trace/redact.js +80 -28
  315. package/dist/trace/self-minted-wire.d.ts +169 -2
  316. package/dist/trace/self-minted-wire.js +54 -1
  317. package/dist/url-refusal.d.ts +1 -1
  318. package/dist/web-admin-client/apply-effective.d.ts +17 -3
  319. package/dist/web-admin-client/apply-effective.js +20 -10
  320. package/dist/web-admin-client/candidate-gate.d.ts +11 -7
  321. package/dist/web-admin-client/candidate-gate.js +1 -1
  322. package/dist/web-admin-client/http-client.d.ts +5 -4
  323. package/dist/web-admin-client/http-client.js +2 -2
  324. package/dist/web-admin-client/read-warnings.d.ts +7 -17
  325. package/dist/web-admin-client/read-warnings.js +5 -9
  326. package/dist/web-admin-client/skills-mcp.js +3 -2
  327. package/dist/web-admin-client/types.d.ts +6 -0
  328. package/package.json +3 -3
  329. package/wire-changes/server@7.139.0.json +348 -0
  330. package/wire-changes/server@7.140.0.json +184 -0
  331. package/dist/http/device-placement.d.ts +0 -175
  332. package/dist/http/device-placement.js +0 -145
@@ -23,7 +23,7 @@ export interface LocalDockerEnvConfig extends LaneHomeConfig {
23
23
  * lane stays `local-docker` (`REMOTE_EXEC=local-docker` + `DOCKER_HOST`; the settings-schema lane table lists
24
24
  * `DOCKER_HOST` as this lane's optional coordinate, and there is no separate `remote-docker` lane since 7.0.0).
25
25
  * Nothing about the class is host-local: the workspace is `mkdir`'d INSIDE the container (never a bind-mount of a
26
- * host path) and bytes move by `docker cp` / `docker exec cat`, which stream over the daemon API.
26
+ * host path) and bytes move by `docker exec -i … cat >` / `docker exec cat`, which stream over the daemon API.
27
27
  */
28
28
  dockerHost?: string;
29
29
  /** Container memory limit (docker `--memory`, e.g. "2g"). Absent = no limit. */
@@ -61,17 +61,11 @@ export interface LocalDockerEnvDeps {
61
61
  spawn?: typeof spawn;
62
62
  }
63
63
  export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv {
64
- /** DUAL-MODE-DESIGN §5: a container is a real isolation boundary; v1 has no managed memory snapshot. */
65
- /** `sharesProcessTable`(core 7.57.0,必填):命令跑在本机容器自己的 PID 命名空间里(不传 `--pid=host`),不在本进程所在的进程表。 */
66
- readonly capabilities: {
67
- isolation: boolean;
68
- suspendable: boolean;
69
- canonicalPathAuthoritative: boolean;
70
- sharesProcessTable: boolean;
71
- };
64
+ /** 能力块的唯一作者 = `capabilitiesFor`(`execution-lane-caps.ts`;逐键表态与理由在那里,本类零能力字面量)。 */
65
+ readonly capabilities: import("@sema-agent/core").ExecutionEnvCapabilities;
72
66
  /** Working directory inside the container; relative paths resolve against it (ExecutionEnv contract). */
73
67
  cwd: string;
74
- /** core `ExecutionEnv.homeDir`([ref]):本容器里执行用户的 home —— 声明胜,否则开容器那一拍实测的 `$HOME`,见 {@link LaneHomeSeat}。读时才取(没声明时连上之前缺席)。 */
68
+ /** core `ExecutionEnv.homeDir`([ref]):本容器里执行用户的 home —— 声明在场 ⇒ 声明(开环境那一拍与 `$HOME` 对证过),否则开容器那一拍实测的 `$HOME`,见 {@link LaneHomeSeat}。读时才取(没声明时连上之前缺席)。 */
75
69
  get homeDir(): string | undefined;
76
70
  /** core 7.49.0 [ref]:shell 子进程真拿到的 `CDPATH` 条目 —— 本车道的命令跑在容器里(镜像的 env),本类不读那份 env ⇒ `"unknown"`(core 契约:说不出就说 unknown,
77
71
  * 不许报一张没读过的空表;删除守卫因此对裸 `cd <相对名>` 之后的删除问人,而不是按「没有 CDPATH」放行)。 */
@@ -92,7 +86,7 @@ export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv
92
86
  private connecting?;
93
87
  /** 家目录:环境答了而与声明不一致 ⇒ 这只环境的拒,记一次、此后每次连接原样回它(同一镜像同一答案:不再起容器、不再问)。 */
94
88
  private homeRefusal?;
95
- /** 家目录的座(声明胜 · 否则开容器那一拍实测 · 实测缺席 ⇒ 缺席 + 一行 warn)。本车道对声明对证。 */
89
+ /** 家目录的座(声明在场 ⇒ 开环境那一拍对证 · 否则开容器那一拍实测 · 实测缺席 ⇒ 缺席 + 一行 warn)。与其余读得出家目录的车道同一只判官、同一句拒。 */
96
90
  private readonly homeSeat;
97
91
  constructor(config: LocalDockerEnvConfig, deps?: LocalDockerEnvDeps);
98
92
  private resolve;
@@ -163,10 +157,27 @@ export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv
163
157
  maxLines?: number;
164
158
  abortSignal?: AbortSignal;
165
159
  }): Promise<Result<string[], FileError>>;
160
+ /** 暂存写的两只面(规则与提交脚本的唯一属主 = `remote-shell.ts` `writeFileStaged` / `appendFileStaged`;父目录与私有暂存目录由编排的准备那一步建):
161
+ * 上传 = 容器内 `cat` 从 stdin 收整份字节落到私有目录里的暂存件({@link pipeIn});提交 = 容器内一次 shell 往返。两者同一身份 ——
162
+ * 同一只 argv 构造({@link execArgv}),都是镜像的用户。不用 `docker cp`:它由守护进程以 root 落盘,镜像用户 ≠ root 时暂存件属 root、
163
+ * 提交方拿不走也清不掉,「目录不收新名字」也探不出来(root 哪里都写得进)。 */
164
+ private stagedLane;
165
+ /** 整旧或整新(暂存 → 一次 `mv -f`)。修前 `docker cp` 直落目标(以守护进程的 root 身份):传输中途断 / 取消时目标剩什么取决于守护进程的解包实现,本适配器不依赖它。 */
166
166
  writeFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
167
+ /** 追加(暂存要加的字节 → 一次 `cat >>`;inode 不换、已有字节不重写)。修前 = 读旧 + 拼 + 整份 cp(O(文件);中途断 ⇒ 目标没了或半份)。
168
+ * 目录不收新名字 / 私有目录收不成私有时的退路 = 容器内 `cat >>` 从 stdin 直接追加到目标(同一身份)。 */
167
169
  appendFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
170
+ /**
171
+ * 把整份内容经 stdin 交给容器里的 `cat`,落到 `dest`(`>` 建或覆盖;`>>` 追加)。走与提交 / 文件面探针同一只往返({@link fsExec},argv 只多一个 `-i`)
172
+ * ⇒ 同一身份、同一工作目录、同一道守卫;二进制安全(载荷不经 shell 转义,也不在宿主落临时件)。stdin 没写完而进程退 0 ⇒ 按失败答(载荷未必整份到了)。
173
+ */
174
+ private pipeIn;
168
175
  private posixFsInst?;
169
176
  private get posixFs();
177
+ /** 文件面的一次容器内 shell 往返(基座、暂存写的准备 / 上传 / 提交 / 清理共用)。承 fsReady 契约(其余三腿走公有 exec、守卫天然在;本腿直用私有
178
+ * docker,必须自带):destroyed 守卫 + 懒连接,fail closed 不抛 —— 消息保留 "destroyed" 字样(测试钉的就是它)。`input` 在场 ⇒ 同一只 argv 多一个
179
+ * `-i`、载荷走 stdin({@link pipeIn});身份与工作目录与不带载荷的往返逐字相同。 */
180
+ private fsExec;
170
181
  fileInfo(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo, FileError>>;
171
182
  listDir(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo[], FileError>>;
172
183
  readLink(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
@@ -200,7 +211,8 @@ export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv
200
211
  /** Prepend `-H <dockerHost>` to a docker argv when configured (per-invocation, not the process env). */
201
212
  private withHostFlag;
202
213
  /** Build the `docker exec` argv for a command, applying cwd (`-w`) and per-command env (`-e K=V`).
203
- * argv elements are SEPARATE (no host-side shell join) — the command runs in the CONTAINER's `/bin/sh -c`. */
214
+ * argv elements are SEPARATE (no host-side shell join) — the command runs in the CONTAINER's `/bin/sh -c`.
215
+ * `stdin` ⇒ `-i` (the payload of {@link pipeIn}); nothing else differs, so a piped upload runs as the same user as every other exec. */
204
216
  private execArgv;
205
217
  /** Like {@link execArgv} but for an internal FileSystem helper command (uses the workspace cwd + adapter env). */
206
218
  private execRaw;
@@ -1,14 +1,14 @@
1
1
  import path from "node:path";
2
- import os from "node:os";
3
- import fs from "node:fs/promises";
4
2
  import { spawn } from "node:child_process";
5
3
  import { hostSubprocessEnv } from "../server-secret-env.js";
6
4
  import { LANE_HOME_PROBE_COMMAND, LaneHomeSeat } from "../lane-home-probe.js";
7
5
  import { randomBytes } from "node:crypto";
8
- import { shellQuote, armPipeDestroyGrace } from "./remote-shell.js";
6
+ import { shellQuote, armPipeDestroyGrace, appendFileStaged, writeFileStaged } from "./remote-shell.js";
9
7
  import { signalNumber } from "./host-platform.js";
10
8
  import { buildLivenessCut, commandBoundsOf } from "../exec-bounds.js";
9
+ import { buildCapturedOutput, createCutError } from "../exec-cut.js";
11
10
  import { FileError, ExecutionError, RemoteExecutionError, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
11
+ import { capabilitiesFor } from "../execution-lane-caps.js";
12
12
  import { fileErrorFromExec, classifyFsStderr } from "./remote-env-file-error.js";
13
13
  import { createPosixShellFs } from "./posix-shell-fs.js";
14
14
  const PROVIDER = "local-docker";
@@ -21,7 +21,7 @@ const unsupported = (op) => ({
21
21
  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)`),
22
22
  });
23
23
  export class RemoteLocalDockerExecutionEnv {
24
- capabilities = { isolation: true, suspendable: false, canonicalPathAuthoritative: true, sharesProcessTable: false };
24
+ capabilities = capabilitiesFor({ remoteExec: { provider: "local-docker" } });
25
25
  cwd;
26
26
  get homeDir() {
27
27
  return this.homeSeat.home;
@@ -50,7 +50,7 @@ export class RemoteLocalDockerExecutionEnv {
50
50
  controlTimeoutMs: config.controlTimeoutMs ?? DEFAULT_CONTROL_TIMEOUT_MS,
51
51
  };
52
52
  this.cwd = this.cfg.mountPath;
53
- this.homeSeat = new LaneHomeSeat({ lane: PROVIDER, declared: config.homeDir, verifiesDeclared: true, onAbsent: config.onHomeAbsent });
53
+ this.homeSeat = new LaneHomeSeat(PROVIDER, config);
54
54
  this.spawnFn = deps.spawn ?? spawn;
55
55
  const id = config.id ?? randomBytes(6).toString("hex");
56
56
  const suffix = randomBytes(4).toString("hex");
@@ -383,71 +383,48 @@ export class RemoteLocalDockerExecutionEnv {
383
383
  lines = lines.slice(0, options.maxLines);
384
384
  return ok(lines);
385
385
  }
386
+ stagedLane(content) {
387
+ return { upload: (dest, signal) => this.pipeIn(content, dest, signal), run: (cmd, signal) => this.fsExec(cmd, signal) };
388
+ }
386
389
  async writeFile(p, content, abortSignal) {
387
390
  const ready = await this.fsReady(p);
388
391
  if (!ready.ok)
389
392
  return ready;
390
- const abs = this.resolve(p);
391
- const dir = path.posix.dirname(abs);
392
- if (dir && dir !== "." && dir !== "/") {
393
- const mk = await this.docker(this.execRaw(`mkdir -p ${shellQuote(dir)}`), { signal: abortSignal, timeoutMs: this.cfg.commandTimeoutMs });
394
- if (!mk.ok)
395
- return { ok: false, error: fileErrorFromExec(mk.error, dir) };
396
- if (mk.value.exitCode !== 0)
397
- return { ok: false, error: classifyFsStderr(mk.value.stderr, dir) };
398
- }
399
- let local;
400
- try {
401
- local = path.join(os.tmpdir(), `docker-cp-${Date.now()}-${randomBytes(6).toString("hex")}`);
402
- await fs.writeFile(local, typeof content === "string" ? Buffer.from(content, "utf8") : Buffer.from(content));
403
- const cp = await this.docker(["cp", local, `${this.containerName}:${abs}`], { signal: abortSignal, timeoutMs: this.cfg.commandTimeoutMs });
404
- if (!cp.ok)
405
- return { ok: false, error: fileErrorFromExec(cp.error, abs) };
406
- if (cp.value.exitCode !== 0)
407
- return { ok: false, error: classifyFsStderr(cp.value.stderr, abs) };
408
- return ok(undefined);
409
- }
410
- catch (e) {
411
- return { ok: false, error: new FileError("unknown", errMsg(e), abs) };
412
- }
413
- finally {
414
- if (local)
415
- await fs.rm(local, { force: true }).catch(() => { });
416
- }
393
+ return writeFileStaged(this.stagedLane(content), this.resolve(p), abortSignal);
417
394
  }
418
395
  async appendFile(p, content, abortSignal) {
396
+ const ready = await this.fsReady(p);
397
+ if (!ready.ok)
398
+ return ready;
419
399
  const abs = this.resolve(p);
420
- const existing = await this.readBinaryFile(abs, abortSignal);
421
- let base;
422
- if (existing.ok)
423
- base = existing.value;
424
- else if (existing.error.code === "not_found")
425
- base = new Uint8Array(0);
426
- else
427
- return existing;
428
- const add = typeof content === "string" ? new TextEncoder().encode(content) : content;
429
- const merged = new Uint8Array(base.length + add.length);
430
- merged.set(base, 0);
431
- merged.set(add, base.length);
432
- return this.writeFile(abs, merged, abortSignal);
400
+ return appendFileStaged(this.stagedLane(content), abs, abortSignal, (ctx) => ctx.inPlace((signal) => this.pipeIn(content, abs, signal, ">>")));
401
+ }
402
+ async pipeIn(content, dest, abortSignal, redirect = ">") {
403
+ const r = await this.fsExec(`cat ${redirect} ${shellQuote(dest)}`, abortSignal, typeof content === "string" ? Buffer.from(content, "utf8") : content);
404
+ if (!r.ok)
405
+ return { ok: false, error: fileErrorFromExec(r.error, dest) };
406
+ if (r.value.exitCode !== 0)
407
+ return { ok: false, error: classifyFsStderr(r.value.stderr, dest) };
408
+ return ok(undefined);
433
409
  }
434
410
  posixFsInst;
435
411
  get posixFs() {
436
412
  return (this.posixFsInst ??= createPosixShellFs({
437
- run: async (cmd, abortSignal) => {
438
- if (this.destroyed)
439
- return { ok: false, error: new ExecutionError("shell_unavailable", "execution env already destroyed") };
440
- if (!this.containerId) {
441
- const c = await this.connect();
442
- if (!c.ok)
443
- return { ok: false, error: new ExecutionError("shell_unavailable", `local-docker connect failed: ${c.error.message}`) };
444
- }
445
- return this.docker(this.execRaw(cmd), { signal: abortSignal, timeoutMs: this.cfg.commandTimeoutMs });
446
- },
413
+ run: (cmd, abortSignal) => this.fsExec(cmd, abortSignal),
447
414
  resolve: (p) => this.resolve(p),
448
415
  tempDir: () => this.cwd,
449
416
  }));
450
417
  }
418
+ async fsExec(cmd, abortSignal, input) {
419
+ if (this.destroyed)
420
+ return { ok: false, error: new ExecutionError("shell_unavailable", "execution env already destroyed") };
421
+ if (!this.containerId) {
422
+ const c = await this.connect();
423
+ if (!c.ok)
424
+ return { ok: false, error: new ExecutionError("shell_unavailable", `local-docker connect failed: ${c.error.message}`, c.error) };
425
+ }
426
+ return this.docker(this.execArgv(cmd, undefined, undefined, input !== undefined), { signal: abortSignal, timeoutMs: this.cfg.commandTimeoutMs, ...(input !== undefined ? { input } : {}) });
427
+ }
451
428
  async fileInfo(p, abortSignal) {
452
429
  return this.posixFs.statFileInfo(p, abortSignal);
453
430
  }
@@ -484,8 +461,8 @@ export class RemoteLocalDockerExecutionEnv {
484
461
  withHostFlag(args) {
485
462
  return this.cfg.dockerHost ? ["-H", this.cfg.dockerHost, ...args] : args;
486
463
  }
487
- execArgv(command, cwd, env) {
488
- const args = ["exec"];
464
+ execArgv(command, cwd, env, stdin = false) {
465
+ const args = stdin ? ["exec", "-i"] : ["exec"];
489
466
  const dir = cwd != null ? this.resolve(cwd) : this.cwd;
490
467
  args.push("-w", dir);
491
468
  const mergedEnv = { ...(this.cfg.env ?? {}), ...(env ?? {}) };
@@ -501,7 +478,7 @@ export class RemoteLocalDockerExecutionEnv {
501
478
  return new Promise((resolve) => {
502
479
  let settled = false;
503
480
  let forceSettleTimer;
504
- const child = this.spawnFn(this.cfg.dockerPath, this.withHostFlag(args), { stdio: ["ignore", "pipe", "pipe"], env: this.cliEnv() });
481
+ const child = this.spawnFn(this.cfg.dockerPath, this.withHostFlag(args), { stdio: [opts?.input !== undefined ? "pipe" : "ignore", "pipe", "pipe"], env: this.cliEnv() });
505
482
  const killGrace = () => {
506
483
  try {
507
484
  child.kill("SIGKILL");
@@ -524,11 +501,11 @@ export class RemoteLocalDockerExecutionEnv {
524
501
  const timeoutMs = opts?.timeoutMs ?? this.cfg.controlTimeoutMs;
525
502
  const timer = setTimeout(() => {
526
503
  killGrace();
527
- finish({ ok: false, error: new ExecutionError("timeout", `docker ${args[0]} exceeded ${timeoutMs}ms`) });
504
+ finish({ ok: false, error: createCutError({ code: "timeout", message: `docker ${args[0]} exceeded ${timeoutMs}ms` }, buildCapturedOutput(outTail, errBuf)) });
528
505
  }, timeoutMs);
529
506
  const onAbort = () => {
530
507
  killGrace();
531
- finish({ ok: false, error: new ExecutionError("aborted", "docker command aborted") });
508
+ finish({ ok: false, error: createCutError({ code: "aborted", message: "docker command aborted" }, buildCapturedOutput(outTail, errBuf)) });
532
509
  };
533
510
  if (opts?.signal) {
534
511
  if (opts.signal.aborted)
@@ -541,10 +518,21 @@ export class RemoteLocalDockerExecutionEnv {
541
518
  outTail.push(d); opts?.onStdout?.(d.toString()); });
542
519
  child.stderr?.on("data", (d) => { errBuf.push(d); opts?.onStderr?.(d.toString()); });
543
520
  child.on("error", (e) => finish({ ok: false, error: new ExecutionError("spawn_error", `docker spawn failed (is '${this.cfg.dockerPath}' installed?): ${e.message}`, e) }));
521
+ let stdinFailure;
522
+ if (opts?.input !== undefined) {
523
+ if (!child.stdin) {
524
+ killGrace();
525
+ return finish({ ok: false, error: new ExecutionError("spawn_error", `docker ${args[0]}: no stdin pipe for the payload`) });
526
+ }
527
+ child.stdin.on("error", (e) => { stdinFailure = e.message; });
528
+ child.stdin.end(Buffer.from(opts.input.buffer, opts.input.byteOffset, opts.input.byteLength));
529
+ }
544
530
  child.on("close", (code, signal) => {
545
531
  if (forceSettleTimer)
546
532
  clearTimeout(forceSettleTimer);
547
533
  const exitCode = code ?? (signal ? 128 + (signalNumber(signal) ?? 9) : 1);
534
+ if (exitCode === 0 && stdinFailure !== undefined)
535
+ return finish({ ok: false, error: new ExecutionError("unknown", `docker ${args[0]}: the payload could not be written to stdin (${stdinFailure})`) });
548
536
  const err = errBuf.result();
549
537
  const stderr = markTruncated(err.text, err.droppedBytes);
550
538
  if (binary) {
@@ -610,7 +598,7 @@ export class RemoteLocalDockerExecutionEnv {
610
598
  return ok(undefined);
611
599
  const c = await this.connect();
612
600
  if (!c.ok)
613
- return { ok: false, error: new FileError("unknown", `local-docker connect failed: ${c.error.message}`, this.resolve(p), c.error) };
601
+ return { ok: false, error: fileErrorFromExec(c.error, this.resolve(p)) };
614
602
  return ok(undefined);
615
603
  }
616
604
  dockerToRemoteError(e, fallback) {
@@ -53,23 +53,8 @@ export interface SshEnvDeps {
53
53
  connectOnce?: (signal?: AbortSignal) => Promise<ConnectResult>;
54
54
  }
55
55
  export declare class RemoteSshExecutionEnv implements RemoteExecutionEnv {
56
- /**
57
- * 🔴 **[ref] / core 7.20.0([ref])`canonicalPathAuthoritative: true`** —— 本适配器的 `canonicalPath`
58
- * 就是**命令真正跑的那块文件系统**的权威答案(`posix-shell-fs` 的 `canonicalPath` 原语,在真机上解到落点)。声明它 = 告诉引擎:整链答不出来是**解析器出了事**
59
- * (RPC 断、传输掉),不是「一个它看不见的命名空间」⇒ 读边界的执行期复核**拒**,而不是让词法判决站着。
60
- * 缺席是今天每个适配器的读法(整链沉默 ⇒ 词法判决成立、命令照跑),所以这一声明是**收紧**方向。
61
- * 严格读法 `=== true`(core `canonicalPathIsAuthoritative`);`isSuspendable` 一族同样严格,对本仓的
62
- * 布尔字面量零影响(四键都写死字面 `true`/`false`,不存在「非布尔值被读成缺席」那一形 —— 那一形由
63
- * core 的 `config.execution_env_capability_invalid` 普查响亮播报,本仓的受众表已登记)。
64
- * 谁该声明由闭集门看着(`test/execution-lane-caps.test.ts` 的逐 lane 对账:新 lane 不表态 = 编译红)。
65
- */
66
- /** `sharesProcessTable`(core 7.57.0,必填):命令跑在 SSH 目标机上;目标恰是本机(或本进程所在容器的宿主)时命令与本进程同表,这一形今天没有配置面可声明 `true`。 */
67
- readonly capabilities: {
68
- isolation: boolean;
69
- suspendable: boolean;
70
- canonicalPathAuthoritative: boolean;
71
- sharesProcessTable: boolean;
72
- };
56
+ /** 能力块的唯一作者 = `capabilitiesFor`(`execution-lane-caps.ts`;逐键表态与理由在那里,本类零能力字面量)。 */
57
+ readonly capabilities: import("@sema-agent/core").ExecutionEnvCapabilities;
73
58
  /** Working directory; relative paths resolve against it (ExecutionEnv contract). Starts at mountPath. */
74
59
  cwd: string;
75
60
  private conn?;
@@ -86,9 +71,9 @@ export declare class RemoteSshExecutionEnv implements RemoteExecutionEnv {
86
71
  * =重连风暴);②共享尝试**不携带任何 caller 信号**(首创者的 pre-abort 不得毒化 joiner;单 caller
87
72
  * 取消只退它自己的等待,底层握手余命 ≤ readyTimeout 有界,彻底取消归 destroy 代际闸)。 */
88
73
  private inFlight?;
89
- /** core `ExecutionEnv.homeDir`([ref]):目标机上登录用户的 home —— 声明胜,否则连上之后实测的 `$HOME`,见 {@link LaneHomeSeat}。读时才取(没声明时连上之前缺席)。 */
74
+ /** core `ExecutionEnv.homeDir`([ref]):目标机上登录用户的 home —— 声明在场 ⇒ 声明(开环境那一拍与 `$HOME` 对证过),否则连上之后实测的 `$HOME`,见 {@link LaneHomeSeat}。读时才取(没声明时连上之前缺席)。 */
90
75
  get homeDir(): string | undefined;
91
- /** 家目录的座(声明胜 · 否则连上之后实测 · 实测缺席 ⇒ 缺席 + 一行 warn)。本车道对声明对证。 */
76
+ /** 家目录的座(声明在场 ⇒ 开环境那一拍对证 · 否则连上之后实测 · 实测缺席 ⇒ 缺席 + 一行 warn)。与其余读得出家目录的车道同一只判官、同一句拒。 */
92
77
  private readonly homeSeat;
93
78
  /** 家目录声明 × 实测的判定,按连接记(同一条连接只问一次;重连换了连接再问 —— 登录用户不变时答案相同,代价一次往返;没声明且已量到绝对路径之后不再问)。 */
94
79
  private homeChecked?;
@@ -186,7 +171,14 @@ export declare class RemoteSshExecutionEnv implements RemoteExecutionEnv {
186
171
  abortSignal?: AbortSignal;
187
172
  }): Promise<Result<string[], FileError>>;
188
173
  readBinaryFile(p: string, abortSignal?: AbortSignal): Promise<Result<Uint8Array, FileError>>;
174
+ /** 暂存写的两只面(规则与提交脚本的唯一属主 = `remote-shell.ts` `writeFileStaged` / `appendFileStaged`;父目录与私有暂存目录由编排的准备那一步建):
175
+ * 上传 = sftp 整份写到私有目录里的暂存件;提交 = 文件面探针 exec。两者同一身份(sftp 子系统与 exec 都是这条连接的登录用户)。
176
+ * sftp 写不收取消信号 ⇒ 取消在传输结束之后、提交之前生效(目标未碰)。 */
177
+ private stagedLane;
178
+ /** 整旧或整新(暂存 → 一次 `mv -f`)。修前 `sftp.writeFile(abs)` 就地 O_TRUNC:传输中途断 ⇒ 目标 = 半份新件。 */
189
179
  writeFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
180
+ /** 追加(暂存要加的字节 → 一次 `cat >>`;inode 不换、已有字节不重写)。修前 = 读旧 + 拼 + 整份就地重写(O(文件);中途断 ⇒ 半份)。
181
+ * 目录不收新名字 / 私有目录收不成私有时的退路 = sftp 直接追加到目标。 */
190
182
  appendFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
191
183
  fileInfo(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo, FileError>>;
192
184
  listDir(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo[], FileError>>;
@@ -1,11 +1,13 @@
1
1
  import path from "node:path";
2
2
  import { Client } from "ssh2";
3
3
  import { createSshHostKeyVerifier } from "../ssh-host-key.js";
4
- import { buildCwdEnvCommand, createCwdGuardedCall, shellQuote, kindFromMode } from "./remote-shell.js";
4
+ import { appendFileStaged, buildCwdEnvCommand, createCwdGuardedCall, kindFromMode, writeFileStaged } from "./remote-shell.js";
5
5
  import { signalNumber } from "./host-platform.js";
6
6
  import { buildLivenessCut, commandBoundsOf } from "../exec-bounds.js";
7
+ import { buildCapturedOutput, createCutError } from "../exec-cut.js";
7
8
  import { createPosixShellFs } from "./posix-shell-fs.js";
8
9
  import { FileError, ExecutionError, RemoteExecutionError, withRetry, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
10
+ import { capabilitiesFor } from "../execution-lane-caps.js";
9
11
  import { fileErrorFromExec } from "./remote-env-file-error.js";
10
12
  import { LANE_HOME_PROBE_COMMAND, LaneHomeSeat } from "../lane-home-probe.js";
11
13
  const PROVIDER = "ssh";
@@ -18,7 +20,7 @@ function exitCodeForSignalKill(signal) {
18
20
  return 128 + (signalNumber(`SIG${signal}`) ?? 9);
19
21
  }
20
22
  export class RemoteSshExecutionEnv {
21
- capabilities = { isolation: false, suspendable: false, canonicalPathAuthoritative: true, sharesProcessTable: false };
23
+ capabilities = capabilitiesFor({ remoteExec: { provider: "ssh" } });
22
24
  cwd;
23
25
  conn;
24
26
  sftpPromise;
@@ -51,7 +53,7 @@ export class RemoteSshExecutionEnv {
51
53
  hostKey: config.hostKey,
52
54
  };
53
55
  this.cwd = this.cfg.mountPath;
54
- this.homeSeat = new LaneHomeSeat({ lane: PROVIDER, declared: config.homeDir, verifiesDeclared: true, onAbsent: config.onHomeAbsent });
56
+ this.homeSeat = new LaneHomeSeat(PROVIDER, config);
55
57
  }
56
58
  resolve(p) {
57
59
  return path.posix.isAbsolute(p) ? path.posix.normalize(p) : path.posix.normalize(path.posix.join(this.cwd, p));
@@ -363,12 +365,29 @@ export class RemoteSshExecutionEnv {
363
365
  return new Promise((resolve) => {
364
366
  const outBuf = new RollingTailBuffer();
365
367
  const errBuf = new RollingTailBuffer();
368
+ const recordStderr = (bytes) => {
369
+ if (bytes.length > 0)
370
+ errBuf.push(bytes);
371
+ };
372
+ const emitStderr = (bytes) => {
373
+ if (bytes.length > 0)
374
+ options?.onStderr?.(bytes.toString());
375
+ };
376
+ const takeStderr = (bytes) => {
377
+ recordStderr(bytes);
378
+ emitStderr(bytes);
379
+ };
380
+ const captured = () => {
381
+ recordStderr(call.endStderr());
382
+ return buildCapturedOutput(outBuf, errBuf);
383
+ };
366
384
  let settled = false;
367
385
  let streamRef;
368
- const finish = (r) => {
386
+ const finish = (outcome) => {
369
387
  if (settled)
370
- return;
388
+ return false;
371
389
  settled = true;
390
+ const r = outcome();
372
391
  clearTimeout(timer);
373
392
  options?.abortSignal?.removeEventListener("abort", onAbort);
374
393
  conn.removeListener("close", onConnClose);
@@ -378,10 +397,14 @@ export class RemoteSshExecutionEnv {
378
397
  catch {
379
398
  }
380
399
  resolve(r);
400
+ return true;
401
+ };
402
+ const cut = (c) => {
403
+ finish(() => ({ ok: false, error: createCutError(c, captured()) }));
381
404
  };
382
- const timer = setTimeout(() => finish({ ok: false, error: new ExecutionError("timeout", `ssh exec exceeded ${timeoutMs}ms (provider/command hang)`) }), timeoutMs);
383
- const onAbort = () => finish({ ok: false, error: new ExecutionError("aborted", "ssh exec aborted") });
384
- const onConnClose = () => finish({ ok: false, error: new ExecutionError("transport_lost", "ssh transport lost mid-command (connection closed before exit status) — reconnect + retry per idempotency") });
405
+ const timer = setTimeout(() => cut({ code: "timeout", message: `ssh exec exceeded ${timeoutMs}ms (provider/command hang)` }), timeoutMs);
406
+ const onAbort = () => cut({ code: "aborted", message: "ssh exec aborted" });
407
+ const onConnClose = () => finish(() => ({ ok: false, error: new ExecutionError("transport_lost", "ssh transport lost mid-command (connection closed before exit status) — reconnect + retry per idempotency") }));
385
408
  conn.once("close", onConnClose);
386
409
  if (options?.abortSignal) {
387
410
  if (options.abortSignal.aborted)
@@ -390,7 +413,7 @@ export class RemoteSshExecutionEnv {
390
413
  }
391
414
  conn.exec(call.command, (err, stream) => {
392
415
  if (err)
393
- return finish({ ok: false, error: new ExecutionError("spawn_error", `ssh exec failed: ${err.message}`, err) });
416
+ return finish(() => ({ ok: false, error: new ExecutionError("spawn_error", `ssh exec failed: ${err.message}`, err) }));
394
417
  streamRef = stream;
395
418
  if (settled) {
396
419
  try {
@@ -407,25 +430,20 @@ export class RemoteSshExecutionEnv {
407
430
  })
408
431
  .on("close", (code, signal) => {
409
432
  if (code == null && !signal) {
410
- return finish({ ok: false, error: new ExecutionError("transport_lost", "ssh channel closed without exit status (transport lost / connection dropped)") });
433
+ return finish(() => ({ ok: false, error: new ExecutionError("transport_lost", "ssh channel closed without exit status (transport lost / connection dropped)") }));
411
434
  }
412
- const tail = call.endStderr();
413
- if (tail.length > 0) {
414
- errBuf.push(tail);
415
- options?.onStderr?.(tail.toString());
416
- }
417
- const out = outBuf.result();
418
- const err = errBuf.result();
419
435
  const exitCode = code ?? (signal ? exitCodeForSignalKill(signal) : 1);
420
- finish(call.settle(ok({ stdout: markTruncated(out.text, out.droppedBytes), stderr: markTruncated(err.text, err.droppedBytes), exitCode })));
436
+ const held = call.endStderr();
437
+ const won = finish(() => {
438
+ recordStderr(held);
439
+ const out = outBuf.result();
440
+ const err = errBuf.result();
441
+ return call.settle(ok({ stdout: markTruncated(out.text, out.droppedBytes), stderr: markTruncated(err.text, err.droppedBytes), exitCode }));
442
+ });
443
+ if (won)
444
+ emitStderr(held);
421
445
  })
422
- .stderr.on("data", (d) => {
423
- const c = call.filterStderr(d);
424
- if (c.length === 0)
425
- return;
426
- errBuf.push(c);
427
- options?.onStderr?.(c.toString());
428
- });
446
+ .stderr.on("data", (d) => takeStderr(call.filterStderr(d)));
429
447
  });
430
448
  });
431
449
  }
@@ -562,7 +580,7 @@ export class RemoteSshExecutionEnv {
562
580
  async sftp(abortSignal) {
563
581
  const ready = await this.ensureConnected(abortSignal);
564
582
  if (!ready.ok)
565
- return Promise.reject(ready.error.code === "aborted" ? new FileError("aborted", ready.error.message) : new Error(ready.error.message));
583
+ return Promise.reject(ready.error.code === "aborted" ? fileErrorFromExec(ready.error) : new FileError("target_unavailable", ready.error.message, undefined, ready.error));
566
584
  if (!this.conn)
567
585
  return Promise.reject(new Error("SSH not connected"));
568
586
  if (!this.sftpPromise) {
@@ -604,41 +622,37 @@ export class RemoteSshExecutionEnv {
604
622
  return { ok: false, error: this.fileError(e, abs) };
605
623
  }
606
624
  }
625
+ stagedLane(data) {
626
+ return {
627
+ upload: async (dest, signal) => {
628
+ try {
629
+ const sftp = await this.sftp(signal);
630
+ await new Promise((resolve, reject) => sftp.writeFile(dest, data, (err) => (err ? reject(err) : resolve())));
631
+ return ok(undefined);
632
+ }
633
+ catch (e) {
634
+ return { ok: false, error: this.fileError(e, dest) };
635
+ }
636
+ },
637
+ run: (cmd, signal) => this.fsExec(cmd, signal),
638
+ };
639
+ }
607
640
  async writeFile(p, content, abortSignal) {
608
- const abs = this.resolve(p);
609
- const dir = path.posix.dirname(abs);
610
- if (dir && dir !== "." && dir !== "/") {
611
- const mk = await this.fsExec(`mkdir -p -- ${shellQuote(dir)}`, abortSignal);
612
- if (!mk.ok)
613
- return { ok: false, error: fileErrorFromExec(mk.error, dir) };
614
- if (mk.value.exitCode !== 0)
615
- return { ok: false, error: new FileError("unknown", mk.value.stderr.trim() || `mkdir -p failed for ${dir}`, dir) };
616
- }
617
- try {
618
- const sftp = await this.sftp(abortSignal);
619
- const data = typeof content === "string" ? Buffer.from(content, "utf8") : Buffer.from(content);
620
- await new Promise((resolve, reject) => sftp.writeFile(abs, data, (err) => (err ? reject(err) : resolve())));
621
- return ok(undefined);
622
- }
623
- catch (e) {
624
- return { ok: false, error: this.fileError(e, abs) };
625
- }
641
+ return writeFileStaged(this.stagedLane(bytesOf(content)), this.resolve(p), abortSignal);
626
642
  }
627
643
  async appendFile(p, content, abortSignal) {
628
644
  const abs = this.resolve(p);
629
- const existing = await this.readBinaryFile(abs, abortSignal);
630
- let base;
631
- if (existing.ok)
632
- base = existing.value;
633
- else if (existing.error.code === "not_found")
634
- base = new Uint8Array(0);
635
- else
636
- return existing;
637
- const add = typeof content === "string" ? new TextEncoder().encode(content) : content;
638
- const merged = new Uint8Array(base.length + add.length);
639
- merged.set(base, 0);
640
- merged.set(add, base.length);
641
- return this.writeFile(abs, merged, abortSignal);
645
+ const data = bytesOf(content);
646
+ return appendFileStaged(this.stagedLane(data), abs, abortSignal, (ctx) => ctx.inPlace(async (signal) => {
647
+ try {
648
+ const sftp = await this.sftp(signal);
649
+ await new Promise((resolve, reject) => sftp.appendFile(abs, data, (err) => (err ? reject(err) : resolve())));
650
+ return ok(undefined);
651
+ }
652
+ catch (e) {
653
+ return { ok: false, error: this.fileError(e, abs) };
654
+ }
655
+ }));
642
656
  }
643
657
  async fileInfo(p, abortSignal) {
644
658
  const abs = this.resolve(p);
@@ -734,4 +748,7 @@ export class RemoteSshExecutionEnv {
734
748
  export function sshExecutionEnvFactory(config) {
735
749
  return (_ctx) => new RemoteSshExecutionEnv(config);
736
750
  }
751
+ function bytesOf(content) {
752
+ return typeof content === "string" ? Buffer.from(content, "utf8") : Buffer.from(content);
753
+ }
737
754
  //# sourceMappingURL=remote-env-ssh.js.map