@sema-agent/server 7.150.0 → 7.151.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 (341) hide show
  1. package/MIGRATION.md +185 -0
  2. package/USAGE.md +53 -25
  3. package/deploy/sema-up/README.md +7 -1
  4. package/deploy/sema-up/chart/Chart.yaml +1 -1
  5. package/deploy/sema-up/chart/templates/NOTES.txt +5 -0
  6. package/deploy/sema-up/chart/templates/_helpers.tpl +15 -16
  7. package/deploy/sema-up/chart/templates/server-v4pro.yaml +5 -3
  8. package/deploy/sema-up/chart/templates/server.yaml +5 -3
  9. package/deploy/sema-up/chart/values-full-single.yaml +3 -1
  10. package/deploy/sema-up/chart/values.yaml +8 -1
  11. package/deploy/sema-up/cluster-up.sh +1 -0
  12. package/deploy/sema-up/kube-up.sh +1 -1
  13. package/deploy/sema-up/lib/flagshape.sh +1 -1
  14. package/deploy/sema-up/lib/helm-values-cluster.py +2 -0
  15. package/deploy/sema-up/lib/helm-values-kube.py +2 -0
  16. package/dist/adoption/import.js +3 -3
  17. package/dist/approval-ask-machine.d.ts +1 -1
  18. package/dist/approval-card.d.ts +2 -2
  19. package/dist/approval-gate-currency.d.ts +1 -1
  20. package/dist/auth-bridge.d.ts +20 -0
  21. package/dist/auth-bridge.js +9 -0
  22. package/dist/background-session-reap.d.ts +1 -1
  23. package/dist/bake-runner/main.d.ts +4 -2
  24. package/dist/bind-host.d.ts +2 -3
  25. package/dist/bind-host.js +2 -4
  26. package/dist/boot/budget-tracing.js +1 -1
  27. package/dist/boot/engine-lease.d.ts +5 -0
  28. package/dist/boot/engine-lease.js +1 -0
  29. package/dist/boot/execution-env.d.ts +8 -14
  30. package/dist/boot/execution-env.js +54 -17
  31. package/dist/boot/execution-lane.js +32 -5
  32. package/dist/boot/late-binds.d.ts +8 -4
  33. package/dist/boot/late-binds.js +7 -1
  34. package/dist/boot/memory-consolidation.d.ts +3 -2
  35. package/dist/boot/memory-consolidation.js +6 -6
  36. package/dist/boot/reapers.d.ts +5 -0
  37. package/dist/boot/reapers.js +12 -1
  38. package/dist/boot/resolve-spec.d.ts +3 -3
  39. package/dist/boot/resolve-spec.js +31 -22
  40. package/dist/boot/runner-deps.d.ts +9 -2
  41. package/dist/boot/runner-deps.js +3 -3
  42. package/dist/boot/session-age-out.d.ts +36 -0
  43. package/dist/boot/session-age-out.js +101 -0
  44. package/dist/boot/session-faces.d.ts +5 -14
  45. package/dist/boot/session-faces.js +9 -5
  46. package/dist/boot/stage-01-config.js +2 -0
  47. package/dist/boot/stage-05-execution-env.d.ts +5 -3
  48. package/dist/boot/stage-05-execution-env.js +2 -2
  49. package/dist/boot/stage-06-runners.d.ts +7 -4
  50. package/dist/boot/stage-06-runners.js +15 -6
  51. package/dist/boot/stage-07-capability-layer.d.ts +7 -4
  52. package/dist/boot/stage-07-capability-layer.js +2 -1
  53. package/dist/boot/stage-08-reapers.d.ts +7 -4
  54. package/dist/boot/stage-08-reapers.js +4 -2
  55. package/dist/boot/stage-09-leader.d.ts +11 -5
  56. package/dist/boot/stage-09-leader.js +12 -3
  57. package/dist/boot/stage-10-http-server.d.ts +11 -5
  58. package/dist/boot/stage-10-http-server.js +3 -6
  59. package/dist/boot/web-admin.js +3 -1
  60. package/dist/boot/workflow-orchestration.d.ts +7 -2
  61. package/dist/boot/workflow-orchestration.js +24 -7
  62. package/dist/bounded-rotation.d.ts +29 -0
  63. package/dist/bounded-rotation.js +24 -0
  64. package/dist/capability-facts.d.ts +22 -13
  65. package/dist/capability-facts.js +28 -18
  66. package/dist/center-credential.d.ts +25 -6
  67. package/dist/center-credential.js +25 -5
  68. package/dist/config-catalog.d.ts +26 -7
  69. package/dist/config-catalog.js +14 -11
  70. package/dist/config-invariants.d.ts +2 -1
  71. package/dist/config-invariants.js +4 -4
  72. package/dist/config-types.d.ts +10 -4
  73. package/dist/config.d.ts +51 -2
  74. package/dist/config.js +35 -24
  75. package/dist/deployment-shape.d.ts +9 -1
  76. package/dist/deployment-shape.js +2 -1
  77. package/dist/device-protocol.d.ts +5 -4
  78. package/dist/device-protocol.js +1 -1
  79. package/dist/device-ws-hub.js +3 -2
  80. package/dist/device-ws-protocol.d.ts +63 -10
  81. package/dist/device-ws-protocol.js +56 -25
  82. package/dist/engine-identity.d.ts +19 -7
  83. package/dist/engine-identity.js +10 -2
  84. package/dist/entry-mode-clamp-notice.d.ts +2 -1
  85. package/dist/execution-env-refusal.d.ts +23 -0
  86. package/dist/execution-env-refusal.js +5 -0
  87. package/dist/execution-lane-caps.d.ts +3 -1
  88. package/dist/field-conflict.d.ts +1 -1
  89. package/dist/file-history-capture.d.ts +11 -4
  90. package/dist/file-history-capture.js +9 -6
  91. package/dist/file-history-disclosure.d.ts +7 -5
  92. package/dist/file-history-disclosure.js +7 -5
  93. package/dist/fleet/drain-entry.d.ts +4 -0
  94. package/dist/hooks/hook-runner.js +2 -2
  95. package/dist/host-lsp-manager.d.ts +2 -2
  96. package/dist/http/admission.js +17 -2
  97. package/dist/http/approval-sweeps.js +5 -5
  98. package/dist/http/checkpoint-ctx.d.ts +2 -0
  99. package/dist/http/checkpoint-ctx.js +1 -0
  100. package/dist/http/dispatch.d.ts +2 -1
  101. package/dist/http/dispatch.js +34 -27
  102. package/dist/http/durable-run-leg.js +16 -16
  103. package/dist/http/health-self-report.d.ts +9 -1
  104. package/dist/http/health-self-report.js +2 -0
  105. package/dist/http/owner-access.d.ts +31 -11
  106. package/dist/http/owner-access.js +37 -6
  107. package/dist/http/resume-ctx-read.js +25 -3
  108. package/dist/http/resume-legs.d.ts +2 -1
  109. package/dist/http/resume-legs.js +259 -79
  110. package/dist/http/route-ctx.d.ts +18 -8
  111. package/dist/http/route-table.d.ts +1 -1
  112. package/dist/http/routes/admin-memory-optout.d.ts +1 -1
  113. package/dist/http/routes/agents-roster.js +2 -3
  114. package/dist/http/routes/approvals-assistant.js +4 -3
  115. package/dist/http/routes/background.d.ts +3 -3
  116. package/dist/http/routes/capabilities.js +8 -5
  117. package/dist/http/routes/fleet.js +10 -8
  118. package/dist/http/routes/projects.d.ts +3 -2
  119. package/dist/http/routes/projects.js +4 -5
  120. package/dist/http/routes/runs.js +102 -36
  121. package/dist/http/routes/session-sync.d.ts +4 -3
  122. package/dist/http/routes/session-sync.js +172 -77
  123. package/dist/http/routes/sessions-list.js +13 -7
  124. package/dist/http/routes/sessions.js +10 -6
  125. package/dist/http/routes/tasks.js +30 -16
  126. package/dist/http/routes/trace-usage.js +2 -2
  127. package/dist/http/server.d.ts +21 -27
  128. package/dist/http/server.js +2 -2
  129. package/dist/http/submit-ctx.js +5 -4
  130. package/dist/http/wire-types.d.ts +14 -4
  131. package/dist/identity-source.d.ts +20 -3
  132. package/dist/identity-source.js +14 -0
  133. package/dist/index.d.ts +4 -0
  134. package/dist/lane-home-probe.d.ts +3 -7
  135. package/dist/lane-home-probe.js +0 -1
  136. package/dist/leader/leader.d.ts +2 -1
  137. package/dist/leg-facts.d.ts +13 -5
  138. package/dist/leg-facts.js +2 -1
  139. package/dist/memory-scope.d.ts +5 -4
  140. package/dist/memory-scope.js +4 -4
  141. package/dist/memory-sync-client.d.ts +30 -2
  142. package/dist/memory-sync-client.js +16 -9
  143. package/dist/observability/err-text.d.ts +7 -0
  144. package/dist/observability/err-text.js +5 -0
  145. package/dist/observability/fail-open.d.ts +7 -3
  146. package/dist/observability/fail-open.js +7 -3
  147. package/dist/observability/metrics.js +1 -0
  148. package/dist/orchestration/workflow-notify-journal.d.ts +18 -1
  149. package/dist/orchestration/workflow-notify-journal.js +12 -1
  150. package/dist/orchestration/workflow-park-decision.d.ts +70 -18
  151. package/dist/orchestration/workflow-park-decision.js +49 -10
  152. package/dist/orchestration/workflow-park-holder.d.ts +65 -0
  153. package/dist/orchestration/workflow-park-holder.js +98 -0
  154. package/dist/orchestration/workflow-record-fence.d.ts +36 -0
  155. package/dist/orchestration/workflow-record-fence.js +42 -0
  156. package/dist/owner-faces.d.ts +1 -1
  157. package/dist/parked-decide.d.ts +104 -53
  158. package/dist/parked-decide.js +153 -117
  159. package/dist/peer-directory.d.ts +2 -2
  160. package/dist/peer-inbound-face.d.ts +2 -6
  161. package/dist/peer-inbound-face.js +0 -3
  162. package/dist/permission-mode-admission.d.ts +41 -5
  163. package/dist/permission-mode-admission.js +29 -3
  164. package/dist/plugins/adoption-log-sql.d.ts +3 -1
  165. package/dist/plugins/adoption-log-sql.js +7 -1
  166. package/dist/plugins/approval-ask-store-file.d.ts +10 -2
  167. package/dist/plugins/approval-ask-store-file.js +8 -0
  168. package/dist/plugins/approval-ask-store-memory.d.ts +6 -1
  169. package/dist/plugins/approval-ask-store-memory.js +28 -9
  170. package/dist/plugins/approval-ask-store-sql.d.ts +43 -2
  171. package/dist/plugins/approval-ask-store-sql.js +34 -5
  172. package/dist/plugins/approval-exemption-store.js +3 -0
  173. package/dist/plugins/approval-nonce-store.js +2 -0
  174. package/dist/plugins/breaker-state-sql.js +2 -0
  175. package/dist/plugins/caching-session-store.d.ts +9 -15
  176. package/dist/plugins/caching-session-store.js +6 -0
  177. package/dist/plugins/device-store-sql.js +6 -0
  178. package/dist/plugins/error-origin.d.ts +18 -0
  179. package/dist/plugins/error-origin.js +10 -0
  180. package/dist/plugins/execution-placement-sql.js +6 -6
  181. package/dist/plugins/image-bake-store-sql.js +6 -0
  182. package/dist/plugins/image-index-sql.js +4 -0
  183. package/dist/plugins/landed-attendants.d.ts +212 -0
  184. package/dist/plugins/landed-attendants.js +161 -0
  185. package/dist/plugins/leader-run-store-sql.js +4 -0
  186. package/dist/plugins/local-session-live-reference.d.ts +40 -0
  187. package/dist/plugins/local-session-live-reference.js +39 -0
  188. package/dist/plugins/local-session-store.d.ts +71 -33
  189. package/dist/plugins/local-session-store.js +129 -27
  190. package/dist/plugins/memory-engine-pg.js +4 -0
  191. package/dist/plugins/memory-engine-sql-apply.js +5 -2
  192. package/dist/plugins/memory-engine-tidb.js +4 -0
  193. package/dist/plugins/memory-optout-grant-store-sql.js +4 -0
  194. package/dist/plugins/memory-sync-store-pg.js +5 -0
  195. package/dist/plugins/memory-sync-store-tidb.js +5 -0
  196. package/dist/plugins/one-time-migrations.d.ts +57 -63
  197. package/dist/plugins/one-time-migrations.js +162 -90
  198. package/dist/plugins/org-delivery-quota-store.js +4 -1
  199. package/dist/plugins/outcome-ledger-sql.js +2 -0
  200. package/dist/plugins/permission-rule-store-sql.js +11 -4
  201. package/dist/plugins/pg-cost-quota.js +4 -0
  202. package/dist/plugins/pg-pool.js +24 -1
  203. package/dist/plugins/pg-rate-limiter.js +2 -0
  204. package/dist/plugins/posix-shell-fs.d.ts +6 -1
  205. package/dist/plugins/posix-shell-fs.js +4 -1
  206. package/dist/plugins/remote-env-adb.d.ts +19 -15
  207. package/dist/plugins/remote-env-adb.js +41 -15
  208. package/dist/plugins/remote-env-connect-seat.d.ts +105 -0
  209. package/dist/plugins/remote-env-connect-seat.js +72 -0
  210. package/dist/plugins/remote-env-device.d.ts +22 -5
  211. package/dist/plugins/remote-env-device.js +106 -8
  212. package/dist/plugins/remote-env-e2b.d.ts +15 -6
  213. package/dist/plugins/remote-env-e2b.js +42 -32
  214. package/dist/plugins/remote-env-file-error.js +6 -2
  215. package/dist/plugins/remote-env-host.d.ts +17 -0
  216. package/dist/plugins/remote-env-host.js +30 -38
  217. package/dist/plugins/remote-env-k8s.d.ts +41 -39
  218. package/dist/plugins/remote-env-k8s.js +53 -30
  219. package/dist/plugins/remote-env-local-docker.d.ts +13 -8
  220. package/dist/plugins/remote-env-local-docker.js +34 -41
  221. package/dist/plugins/remote-env-ssh.d.ts +18 -12
  222. package/dist/plugins/remote-env-ssh.js +60 -59
  223. package/dist/plugins/remote-shell.d.ts +98 -1
  224. package/dist/plugins/remote-shell.js +218 -0
  225. package/dist/plugins/resume-anchor-store-sql.d.ts +11 -2
  226. package/dist/plugins/resume-anchor-store-sql.js +12 -5
  227. package/dist/plugins/retention-lane-store-sql.js +4 -0
  228. package/dist/plugins/retention-sql.d.ts +20 -2
  229. package/dist/plugins/retention-sql.js +11 -0
  230. package/dist/plugins/run-store-sql.d.ts +21 -2
  231. package/dist/plugins/run-store-sql.js +31 -7
  232. package/dist/plugins/scenario-prefix-switch-m42.d.ts +1 -1
  233. package/dist/plugins/scenario-prefix-switch-m42.js +2 -2
  234. package/dist/plugins/schema-consistency.d.ts +4 -2
  235. package/dist/plugins/schema-consistency.js +19 -2
  236. package/dist/plugins/send-file-ledger.js +3 -0
  237. package/dist/plugins/serving-requirement.d.ts +26 -7
  238. package/dist/plugins/serving-requirement.js +11 -4
  239. package/dist/plugins/session-birth.d.ts +6 -2
  240. package/dist/plugins/session-capture-record-store-sql.js +2 -0
  241. package/dist/plugins/session-ext-table.d.ts +14 -1
  242. package/dist/plugins/session-ext-table.js +5 -1
  243. package/dist/plugins/session-row-sql.d.ts +49 -2
  244. package/dist/plugins/session-row-sql.js +56 -13
  245. package/dist/plugins/session-store-sql.d.ts +34 -18
  246. package/dist/plugins/session-store-sql.js +98 -17
  247. package/dist/plugins/shared-memory-store-sql.d.ts +1 -0
  248. package/dist/plugins/shared-memory-store-sql.js +6 -2
  249. package/dist/plugins/sql-driver.d.ts +2 -1
  250. package/dist/plugins/sql-driver.js +18 -3
  251. package/dist/plugins/sql-errors.d.ts +11 -6
  252. package/dist/plugins/sql-errors.js +5 -6
  253. package/dist/plugins/stop-then-start-sentence.d.ts +10 -4
  254. package/dist/plugins/stop-then-start-sentence.js +14 -10
  255. package/dist/plugins/stop-then-start.d.ts +109 -0
  256. package/dist/plugins/stop-then-start.js +104 -0
  257. package/dist/plugins/store-backend.d.ts +40 -13
  258. package/dist/plugins/store-backend.js +169 -27
  259. package/dist/plugins/store-boundary.d.ts +57 -0
  260. package/dist/plugins/store-boundary.js +139 -0
  261. package/dist/plugins/store-contracts.d.ts +17 -0
  262. package/dist/plugins/store-fault-sentence-m53.d.ts +7 -0
  263. package/dist/plugins/store-fault-sentence-m53.js +25 -0
  264. package/dist/plugins/store-fault.d.ts +26 -7
  265. package/dist/plugins/store-fault.js +18 -2
  266. package/dist/plugins/task-attachment-store.js +9 -0
  267. package/dist/plugins/task-list-store-sql.d.ts +2 -2
  268. package/dist/plugins/task-list-store-sql.js +10 -8
  269. package/dist/plugins/tidb-cost-quota.js +4 -0
  270. package/dist/plugins/tidb-pool.js +2 -1
  271. package/dist/plugins/tidb-rate-limiter.js +4 -0
  272. package/dist/plugins/user-input-ledger-sql.js +4 -0
  273. package/dist/plugins/workflow-run-store-sql.d.ts +28 -10
  274. package/dist/plugins/workflow-run-store-sql.js +64 -4
  275. package/dist/plugins/workspace-fill.d.ts +4 -7
  276. package/dist/plugins/workspace-fill.js +2 -1
  277. package/dist/plugins/write-behind-counter.js +5 -0
  278. package/dist/project-context.d.ts +0 -2
  279. package/dist/project-context.js +4 -8
  280. package/dist/project-registry.d.ts +46 -0
  281. package/dist/project-registry.js +44 -0
  282. package/dist/request-key-closure.js +1 -1
  283. package/dist/request-value-closure.d.ts +4 -0
  284. package/dist/request-value-closure.js +10 -0
  285. package/dist/run-acceptance.d.ts +3 -2
  286. package/dist/run-acceptance.js +4 -6
  287. package/dist/run-local.js +1 -0
  288. package/dist/runs.d.ts +37 -8
  289. package/dist/runs.js +53 -12
  290. package/dist/runtime-governance.d.ts +5 -4
  291. package/dist/security.d.ts +30 -32
  292. package/dist/security.js +41 -7
  293. package/dist/server-secret-env.d.ts +12 -10
  294. package/dist/server-secret-env.js +24 -15
  295. package/dist/session-bindings.d.ts +82 -0
  296. package/dist/session-bindings.js +18 -0
  297. package/dist/session-children-retire.d.ts +2 -2
  298. package/dist/session-children-retire.js +2 -6
  299. package/dist/session-deletion.d.ts +16 -1
  300. package/dist/session-error-wire.d.ts +3 -3
  301. package/dist/session-error-wire.js +3 -3
  302. package/dist/session-incarnation.d.ts +10 -0
  303. package/dist/session-incarnation.js +10 -1
  304. package/dist/session-kind.d.ts +57 -0
  305. package/dist/session-kind.js +22 -0
  306. package/dist/session-row.d.ts +21 -1
  307. package/dist/session-row.js +7 -1
  308. package/dist/session-sync-kernel.d.ts +24 -14
  309. package/dist/session-sync-kernel.js +3 -6
  310. package/dist/session-sync.d.ts +4 -98
  311. package/dist/session-sync.js +1 -71
  312. package/dist/session-titler.d.ts +10 -3
  313. package/dist/session-titler.js +19 -6
  314. package/dist/ssh-host-key.d.ts +5 -0
  315. package/dist/ssh-host-key.js +3 -0
  316. package/dist/stores-sql/file-history-store-sql.d.ts +52 -2
  317. package/dist/stores-sql/file-history-store-sql.js +108 -70
  318. package/dist/stores-sql/pg-session-storage.d.ts +34 -7
  319. package/dist/stores-sql/pg-session-storage.js +37 -13
  320. package/dist/stores-sql/retention-store-sql.d.ts +1 -9
  321. package/dist/stores-sql/retention-store-sql.js +2 -10
  322. package/dist/stores-sql/session-policy-store-sql.d.ts +30 -7
  323. package/dist/stores-sql/session-policy-store-sql.js +61 -44
  324. package/dist/stores-sql/session-write-hooks.d.ts +11 -0
  325. package/dist/stores-sql/tidb-session-store.d.ts +58 -7
  326. package/dist/stores-sql/tidb-session-store.js +37 -13
  327. package/dist/submission-exit.d.ts +3 -2
  328. package/dist/submission-exit.js +9 -1
  329. package/dist/task-cwd.d.ts +2 -2
  330. package/dist/task-settings.d.ts +6 -5
  331. package/dist/terminal.d.ts +33 -2
  332. package/dist/terminal.js +139 -9
  333. package/dist/tool-approval.d.ts +140 -6
  334. package/dist/tool-approval.js +165 -72
  335. package/dist/trace/self-minted-wire.d.ts +290 -27
  336. package/dist/trace/self-minted-wire.js +101 -13
  337. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +78 -27
  338. package/package.json +2 -2
  339. package/wire-changes/server@7.151.0.json +762 -0
  340. package/dist/http/session-inherit-env.d.ts +0 -36
  341. package/dist/http/session-inherit-env.js +0 -6
@@ -13,6 +13,7 @@ import { FileError, ExecutionError, RemoteExecutionError, RollingTailBuffer, mar
13
13
  import { capabilitiesFor } from "../execution-lane-caps.js";
14
14
  import { ADAPTER_START, BOOTSTRAP_START, createSandboxReadFloor, dockerIdentityFlags, engineStart, floorArgv, } from "./sandbox-read-floor.js";
15
15
  import { fileErrorFromExec, classifyFsStderr } from "./remote-env-file-error.js";
16
+ import { connectRefusal, createConnectSeat, withdrawOnAbort } from "./remote-env-connect-seat.js";
16
17
  import { createPosixShellFs } from "./posix-shell-fs.js";
17
18
  const PROVIDER = "local-docker";
18
19
  const DEFAULT_MOUNT_PATH = "/workspace";
@@ -49,7 +50,11 @@ export class RemoteLocalDockerExecutionEnv {
49
50
  handle;
50
51
  destroyed = false;
51
52
  connecting;
52
- connectRefusal;
53
+ seat = createConnectSeat({
54
+ live: () => (this.containerId !== undefined ? this.workspaceHandle() : undefined),
55
+ inFlight: () => this.connecting,
56
+ establish: (config) => this.connectOnce(config, "start-or-attach"),
57
+ });
53
58
  homeSeat;
54
59
  lifecycle = new AbortController();
55
60
  capacityDeadline;
@@ -100,20 +105,15 @@ export class RemoteLocalDockerExecutionEnv {
100
105
  return unsupported("connect(snapshotId)");
101
106
  if (this.destroyed)
102
107
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env already destroyed") };
103
- if (this.connectRefusal)
104
- return { ok: false, error: this.connectRefusal };
105
- if (config?.abortSignal?.aborted)
106
- return { ok: false, error: new RemoteExecutionError("aborted", "connect aborted") };
107
- if (this.containerId)
108
- return ok(this.workspaceHandle());
109
- return this.connectOnce(config, "start-or-attach");
108
+ return this.seat.connect(config);
110
109
  }
111
110
  async connectOnce(config, mode) {
112
111
  if (this.connecting)
113
112
  return this.connecting;
114
- this.connecting = this.doConnect(config, mode);
115
- const r = await this.connecting;
116
- if (!r.ok)
113
+ const mine = this.doConnect(config, mode);
114
+ this.connecting = mine;
115
+ const r = await mine;
116
+ if (this.connecting === mine)
117
117
  this.connecting = undefined;
118
118
  return r;
119
119
  }
@@ -230,11 +230,8 @@ export class RemoteLocalDockerExecutionEnv {
230
230
  if (this.destroyed)
231
231
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env destroyed during connect") };
232
232
  const filled = await this.fillWorkspace("restored");
233
- if (!filled.ok) {
234
- if (filled.standing)
235
- this.connectRefusal = filled.error;
236
- return { ok: false, error: filled.error };
237
- }
233
+ if (!filled.ok)
234
+ return this.seat.answer(filled);
238
235
  if (this.destroyed)
239
236
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env destroyed during connect") };
240
237
  this.containerId = id;
@@ -299,9 +296,7 @@ export class RemoteLocalDockerExecutionEnv {
299
296
  const filled = await this.fillWorkspace("minted");
300
297
  if (!filled.ok || this.destroyed) {
301
298
  await this.removeInstance(id);
302
- if (!filled.ok && filled.standing)
303
- this.connectRefusal = filled.error;
304
- return { ok: false, error: filled.ok ? new RemoteExecutionError("connect_failed", "execution env destroyed during connect") : filled.error };
299
+ return filled.ok ? { ok: false, error: new RemoteExecutionError("connect_failed", "execution env destroyed during connect") } : this.seat.answer(filled);
305
300
  }
306
301
  this.containerId = id;
307
302
  this.handle = { sandboxId: this.containerName, provider: PROVIDER, mountPath: this.cfg.mountPath, sessionToken: this.containerName };
@@ -356,10 +351,7 @@ export class RemoteLocalDockerExecutionEnv {
356
351
  const verdict = this.homeSeat.settle(probe.ok && probe.value.exitCode === 0 ? { ok: true, stdout: probe.value.stdout } : { ok: false, reason: probe.ok ? `exit ${probe.value.exitCode}` : probe.error.message });
357
352
  if (verdict === undefined)
358
353
  return undefined;
359
- const error = new RemoteExecutionError("connect_failed", verdict.refusal);
360
- if (verdict.answered)
361
- this.connectRefusal = error;
362
- return error;
354
+ return verdict.answered ? this.seat.answer(connectRefusal("home_unconfirmed", verdict.refusal)).error : new RemoteExecutionError("connect_failed", verdict.refusal);
363
355
  }
364
356
  async suspendVM(_options) {
365
357
  return unsupported("suspendVM");
@@ -373,8 +365,9 @@ export class RemoteLocalDockerExecutionEnv {
373
365
  async reconnect(sessionToken) {
374
366
  if (this.destroyed)
375
367
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env already destroyed") };
376
- if (this.connectRefusal)
377
- return { ok: false, error: this.connectRefusal };
368
+ const held = this.seat.held;
369
+ if (held)
370
+ return held;
378
371
  if (sessionToken && sessionToken !== this.containerName) {
379
372
  return { ok: false, error: new RemoteExecutionError("connect_failed", `reconnect token '${String(sessionToken)}' does not match this env's container '${this.containerName}'`) };
380
373
  }
@@ -666,7 +659,7 @@ export class RemoteLocalDockerExecutionEnv {
666
659
  return appendFileStaged(this.stagedLane(content), abs, abortSignal, readyUpload((dest, signal) => this.pipeIn(content, dest, signal, ">>")));
667
660
  }
668
661
  async pipeIn(content, dest, abortSignal, redirect = ">") {
669
- const r = await this.fsExec(`cat ${redirect} ${shellQuote(dest)}`, abortSignal, typeof content === "string" ? Buffer.from(content, "utf8") : content);
662
+ const r = await this.fsExec(`cat ${redirect} ${shellQuote(dest)}`, abortSignal, { input: typeof content === "string" ? Buffer.from(content, "utf8") : content });
670
663
  if (!r.ok)
671
664
  return { ok: false, error: fileErrorFromExec(r.error, dest) };
672
665
  if (r.value.exitCode !== 0)
@@ -679,9 +672,18 @@ export class RemoteLocalDockerExecutionEnv {
679
672
  run: (cmd, abortSignal) => this.fsExec(cmd, abortSignal),
680
673
  resolve: (p) => this.resolve(p),
681
674
  tempDir: () => this.cwd,
675
+ boundLane: {
676
+ run: async (cmd, _cap, abortSignal) => {
677
+ const r = await this.fsExec(cmd, abortSignal, { binary: true });
678
+ return r.ok ? { ok: true, value: { stdout: r.value.stdoutBytes ?? new Uint8Array(), stderr: r.value.stderr, exitCode: r.value.exitCode } } : r;
679
+ },
680
+ payload: "raw",
681
+ boundSec: this.cfg.commandTimeoutMs / 1000,
682
+ },
682
683
  }));
683
684
  }
684
- async fsExec(cmd, abortSignal, input) {
685
+ async fsExec(cmd, abortSignal, o = {}) {
686
+ const input = o.input;
685
687
  if (this.destroyed)
686
688
  return { ok: false, error: new ExecutionError("shell_unavailable", "execution env already destroyed") };
687
689
  if (!this.containerId) {
@@ -694,7 +696,7 @@ export class RemoteLocalDockerExecutionEnv {
694
696
  const argv = this.startArgv(ADAPTER_START, cmd, { stdin: input !== undefined });
695
697
  if (!argv.ok)
696
698
  return argv;
697
- return this.docker(argv.value, { signal: abortSignal, timeoutMs: this.cfg.commandTimeoutMs, ...(input !== undefined ? { input } : {}) });
699
+ return this.docker(argv.value, { signal: abortSignal, timeoutMs: this.cfg.commandTimeoutMs, ...(input !== undefined ? { input } : {}), ...(o.binary === true ? { binary: true } : {}) });
698
700
  }
699
701
  async fileInfo(p, abortSignal) {
700
702
  return this.posixFs.statFileInfo(p, abortSignal);
@@ -708,6 +710,9 @@ export class RemoteLocalDockerExecutionEnv {
708
710
  async canonicalChain(p, abortSignal) {
709
711
  return this.posixFs.canonicalChain(p, abortSignal);
710
712
  }
713
+ async openBound(p, abortSignal) {
714
+ return this.posixFs.openBound(p, abortSignal);
715
+ }
711
716
  async canonicalPath(p, abortSignal) {
712
717
  return this.posixFs.canonicalPath(p, abortSignal);
713
718
  }
@@ -882,19 +887,7 @@ export class RemoteLocalDockerExecutionEnv {
882
887
  });
883
888
  }
884
889
  joinConnect(signal) {
885
- const shared = this.connect();
886
- if (signal === undefined)
887
- return shared;
888
- if (signal.aborted)
889
- return Promise.resolve({ ok: false, error: new RemoteExecutionError("aborted", "aborted while connecting") });
890
- return new Promise((resolve) => {
891
- const onAbort = () => resolve({ ok: false, error: new RemoteExecutionError("aborted", "aborted while connecting") });
892
- signal.addEventListener("abort", onAbort, { once: true });
893
- void shared.then((r) => {
894
- signal.removeEventListener("abort", onAbort);
895
- resolve(r);
896
- });
897
- });
890
+ return withdrawOnAbort(this.connect(), signal);
898
891
  }
899
892
  containerShell = "/bin/sh";
900
893
  containerShellReady;
@@ -2,6 +2,7 @@ import { type SshHostKeyPolicy } from "../ssh-host-key.js";
2
2
  import { type LaneLogger } from "./remote-shell.js";
3
3
  import { FileError, ExecutionError, RemoteExecutionError, type ExecutionEnv, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type SymlinkChain, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type VmLifecycleOptions, type ExecutionEnvFactory } from "@sema-agent/core";
4
4
  import { type LaneHomeConfig } from "../lane-home-probe.js";
5
+ import { type ConnectResult } from "./remote-env-connect-seat.js";
5
6
  type ExecOpts = Parameters<ExecutionEnv["exec"]>[1];
6
7
  export interface SshEnvConfig extends LaneHomeConfig {
7
8
  host: string;
@@ -40,13 +41,6 @@ export interface SshEnvConfig extends LaneHomeConfig {
40
41
  /** 运维日志座(起服装配递本服务的 logger):暂存写「已有目标的权限位没带过去」那一行经它(缺席 ⇒ 暂存编排自建一只,见 `remote-shell.ts` `StagedWriteLane.logger`)。 */
41
42
  logger?: LaneLogger;
42
43
  }
43
- type ConnectResult = {
44
- ok: true;
45
- value: WorkspaceHandle;
46
- } | {
47
- ok: false;
48
- error: RemoteExecutionError;
49
- };
50
44
  /**
51
45
  * Test seam ([ref] §9.3 fault-injection): inject a `connectOnce` that replays an error sequence, so the
52
46
  * retry policy (retryable codes / maxAttempts / "first fail → second success") is deterministically testable
@@ -80,8 +74,12 @@ export declare class RemoteSshExecutionEnv implements RemoteExecutionEnv {
80
74
  private readonly homeSeat;
81
75
  /** 家目录声明 × 实测的判定,按连接记(同一条连接只问一次;重连换了连接再问 —— 登录用户不变时答案相同,代价一次往返;没声明且已量到绝对路径之后不再问)。 */
82
76
  private homeChecked?;
83
- /** 目标机答了而与声明不一致 ⇒ 这只环境的拒,记一次、此后每个面原样回它(同一目标机同一登录用户同一答案:不再问、不再发命令)。 */
84
- private homeRefusal?;
77
+ /**
78
+ * 建连座(`remote-env-connect-seat.ts`):`connect` 的唯一次序(记住的拒 ⇒ 原样再答 · 同代际的连接在飞 ⇒ 等它,调用方的取消只退它自己的等待 · 已连 ⇒ 当下的把手 · 否则起一条),
79
+ * 并持有这只环境记住的拒 —— 两件立定的事实:目标机答了家目录而与声明不一致(`env.home_unconfirmed`;同一目标机同一登录用户同一答案:不再问、不再发命令),
80
+ * 目标机出示的主机密钥过不了部署钉的判(`env.host_key_rejected`;同一配置同一主机每次同答:不再拨号)。此后每个面原样回它。
81
+ */
82
+ private readonly seat;
85
83
  /** core 7.49.0 [ref]:shell 子进程真拿到的 `CDPATH` 条目 —— 本车道的命令跑在远端登录 shell 里(目标机的 profile / env),本类不读那份 env ⇒ `"unknown"`(core 契约:说不出就说 unknown,
86
84
  * 不许报一张没读过的空表;删除守卫因此对裸 `cd <相对名>` 之后的删除问人,而不是按「没有 CDPATH」放行)。 */
87
85
  readonly cdPath: "unknown";
@@ -100,8 +98,8 @@ export declare class RemoteSshExecutionEnv implements RemoteExecutionEnv {
100
98
  ok: false;
101
99
  error: RemoteExecutionError;
102
100
  }>;
103
- /** 五轮复审:per-caller 可中止等待(共享诺不动,只退本 caller)。 */
104
- private raceConnAbort;
101
+ /** 一只建连的拒在 exec / 文件面上的那一句:拒句原文(cause = 那只拒,文件面据它答 `target_unavailable`)。 */
102
+ private refusalOnExecFace;
105
103
  /** [ref] 四轮复审:三路(connect/reconnect/懒连接)唯一的连接发起点——同代际在飞记录 join,settle
106
104
  * 身份清;代际抓拍一次贯穿全部重试轮,每轮起跳先查(陈旧/destroyed=不再开 socket,用不可重试的
107
105
  * aborted 码即刻收敛 withRetry)。五轮复审:共享尝试零 caller 信号(取消面全在 per-caller race)。 */
@@ -162,8 +160,16 @@ export declare class RemoteSshExecutionEnv implements RemoteExecutionEnv {
162
160
  /** 文件面探针的 exec(车PL 验修 ③):只用绝对路径,**不进工作目录**、没有 cwd 守卫 —— 工作目录进不去时探针照样答真话(`permission_denied` /
163
161
  * `not_found` 由命令自己的 stderr 归),core 的 `isDeadCwd` 不会把「进不去」读成「没了」。 */
164
162
  private fsExec;
165
- /** buffered exec 的唯一体:`call` 定了整串、stderr 的 started 行剥法与收口(`remote-shell.ts` `createCwdGuardedCall`)。 */
163
+ /** buffered exec(命令面 + 文件面探针):stdout 走 8 MiB 滚动尾(模型的 `yes` / `cat big` 撑不爆 worker),截掉的字节在结果里如实标出。 */
166
164
  private execCall;
165
+ /**
166
+ * 绑定读一答的往返(`remote-shell.ts` 的脚本经它跑):与文件面探针同一起动(不进工作目录、没有 cwd 守卫)、数据界;stdout **逐字节**收、只留开头
167
+ * `cap + 1` 字节 —— 本答该有多长由读法定(`cap`),比它长的答由读法拒,从不解码一段尾巴(命令面那只 8 MiB 滚动尾在这里不许用)。
168
+ */
169
+ private boundExec;
170
+ /** buffered exec 的唯一体:`call` 定了整串、stderr 的 started 行剥法与收口(`remote-shell.ts` `createCwdGuardedCall`);stdout 的收法由 `sink` 定
171
+ * (命令面 = 滚动尾;绑定读 = 封顶的逐字节头)。`sink.tail` = 切断时随错误带出的 stdout(绑定读不带)。 */
172
+ private runBuffered;
167
173
  execStream(command: string, options?: ExecStreamOptions): AsyncIterable<OutputChunk>;
168
174
  private sftp;
169
175
  absolutePath(p: string): Promise<Result<string, FileError>>;
@@ -11,6 +11,7 @@ import { FileError, ExecutionError, RemoteExecutionError, withRetry, RollingTail
11
11
  import { capabilitiesFor } from "../execution-lane-caps.js";
12
12
  import { fileErrorFromExec } from "./remote-env-file-error.js";
13
13
  import { LANE_HOME_PROBE_COMMAND, LaneHomeSeat } from "../lane-home-probe.js";
14
+ import { connectRefusal, createConnectSeat, withdrawOnAbort } from "./remote-env-connect-seat.js";
14
15
  const PROVIDER = "ssh";
15
16
  const ok = (value) => ({ ok: true, value });
16
17
  const unsupported = (op) => ({
@@ -33,7 +34,11 @@ export class RemoteSshExecutionEnv {
33
34
  }
34
35
  homeSeat;
35
36
  homeChecked;
36
- homeRefusal;
37
+ seat = createConnectSeat({
38
+ live: () => (this.conn !== undefined && !this.destroyed ? this.workspaceHandle() : undefined),
39
+ inFlight: () => (this.inFlight !== undefined && this.inFlight.epoch === this.connEpoch ? this.inFlight.promise : undefined),
40
+ establish: (config) => withdrawOnAbort(this.startConnect("establish"), config?.abortSignal),
41
+ });
37
42
  cdPath = "unknown";
38
43
  withheldEnv = [];
39
44
  cfg;
@@ -66,26 +71,10 @@ export class RemoteSshExecutionEnv {
66
71
  async connect(config) {
67
72
  if (this.destroyed)
68
73
  return { ok: false, error: new RemoteExecutionError("connect_failed", "SSH env destroyed — create a fresh env instead of reviving") };
69
- if (this.conn)
70
- return ok(this.workspaceHandle());
71
- return this.raceConnAbort(this.startConnect("establish"), config?.abortSignal);
74
+ return this.seat.connect(config);
72
75
  }
73
- async raceConnAbort(shared, signal) {
74
- if (!signal)
75
- return shared;
76
- if (signal.aborted)
77
- return { ok: false, error: new RemoteExecutionError("aborted", "connect aborted") };
78
- let onAbort;
79
- const aborted = new Promise((res) => {
80
- onAbort = () => res("aborted");
81
- signal.addEventListener("abort", onAbort, { once: true });
82
- });
83
- const raced = await Promise.race([shared, aborted]);
84
- if (onAbort)
85
- signal.removeEventListener("abort", onAbort);
86
- if (raced === "aborted")
87
- return { ok: false, error: new RemoteExecutionError("aborted", "connect aborted while waiting") };
88
- return raced;
76
+ refusalOnExecFace(refusal) {
77
+ return new ExecutionError("shell_unavailable", refusal.error.message, refusal.error);
89
78
  }
90
79
  startConnect(kind) {
91
80
  if (this.inFlight && this.inFlight.epoch === this.connEpoch)
@@ -115,10 +104,15 @@ export class RemoteSshExecutionEnv {
115
104
  }
116
105
  async ensureConnected(signal) {
117
106
  for (let round = 0; round < 3; round++) {
118
- if (this.homeRefusal)
119
- return { ok: false, error: this.homeRefusal };
107
+ const held = this.seat.held;
108
+ if (held)
109
+ return { ok: false, error: this.refusalOnExecFace(held) };
120
110
  const transport = await this.ensureTransport(signal);
121
- if (!transport.ok || !this.homeSeat.asks)
111
+ if (!transport.ok) {
112
+ const now = this.seat.held;
113
+ return now ? { ok: false, error: this.refusalOnExecFace(now) } : transport;
114
+ }
115
+ if (!this.homeSeat.asks)
122
116
  return transport;
123
117
  const conn = this.conn;
124
118
  if (!conn)
@@ -127,7 +121,7 @@ export class RemoteSshExecutionEnv {
127
121
  const checked = { conn, verdict: this.homeVerdict(conn) };
128
122
  this.homeChecked = checked;
129
123
  void checked.verdict.then((r) => {
130
- if (!r.ok && this.homeRefusal === undefined && this.homeChecked === checked)
124
+ if (!r.ok && this.seat.held === undefined && this.homeChecked === checked)
131
125
  this.homeChecked = undefined;
132
126
  });
133
127
  }
@@ -185,10 +179,9 @@ export class RemoteSshExecutionEnv {
185
179
  const verdict = this.homeSeat.settle(probe);
186
180
  if (verdict === undefined)
187
181
  return resolve(ok(undefined));
188
- const error = new ExecutionError("shell_unavailable", verdict.refusal);
189
182
  if (verdict.answered)
190
- this.homeRefusal = error;
191
- resolve({ ok: false, error });
183
+ return resolve({ ok: false, error: this.refusalOnExecFace(this.seat.answer(connectRefusal("home_unconfirmed", verdict.refusal))) });
184
+ resolve({ ok: false, error: new ExecutionError("shell_unavailable", verdict.refusal) });
192
185
  };
193
186
  const timer = setTimeout(() => settle({ ok: false, reason: `no answer within ${this.cfg.readyTimeoutMs}ms` }), this.cfg.readyTimeoutMs);
194
187
  try {
@@ -222,33 +215,14 @@ export class RemoteSshExecutionEnv {
222
215
  return { ok: false, error: new ExecutionError("shell_unavailable", "SSH env destroyed — no lazy reconnect after teardown") };
223
216
  if (signal?.aborted)
224
217
  return { ok: false, error: new ExecutionError("aborted", "aborted before SSH lazy-connect") };
225
- if (this.conn)
226
- return ok(undefined);
227
- const shared = this.startConnect("establish");
228
- let r;
229
- if (signal) {
230
- let onAbort;
231
- const aborted = new Promise((res) => {
232
- onAbort = () => res("aborted");
233
- signal.addEventListener("abort", onAbort, { once: true });
234
- });
235
- const raced = await Promise.race([shared, aborted]);
236
- if (onAbort)
237
- signal.removeEventListener("abort", onAbort);
238
- if (raced === "aborted")
239
- return { ok: false, error: new ExecutionError("aborted", "aborted while waiting for SSH lazy-connect") };
240
- r = raced;
241
- }
242
- else {
243
- r = await shared;
244
- }
218
+ const r = await this.seat.connect(signal ? { abortSignal: signal } : undefined);
245
219
  if (r.ok) {
246
220
  if (this.conn)
247
221
  return ok(undefined);
248
222
  return { ok: false, error: new ExecutionError("shell_unavailable", "SSH connection dropped right after connect") };
249
223
  }
250
224
  if (r.error.code === "aborted")
251
- return { ok: false, error: new ExecutionError("aborted", r.error.message, r.error) };
225
+ return { ok: false, error: new ExecutionError("aborted", signal?.aborted ? "aborted while waiting for SSH lazy-connect" : r.error.message, r.error) };
252
226
  return { ok: false, error: new ExecutionError("shell_unavailable", `SSH lazy-connect failed: ${r.error.message}`, r.error) };
253
227
  }
254
228
  connectOnce(signal, opEpoch) {
@@ -273,7 +247,7 @@ export class RemoteSshExecutionEnv {
273
247
  hostVerifier: (key) => {
274
248
  const verdict = verifyHostKey(key);
275
249
  if (!verdict.ok) {
276
- hostKeyRejection = verdict.reason;
250
+ hostKeyRejection = { reason: verdict.reason, standing: verdict.standing };
277
251
  return false;
278
252
  }
279
253
  return true;
@@ -303,10 +277,9 @@ export class RemoteSshExecutionEnv {
303
277
  }
304
278
  catch {
305
279
  }
306
- resolve({
307
- ok: false,
308
- error: hostKeyRejection !== undefined ? new RemoteExecutionError("connect_failed", hostKeyRejection, e) : this.connErr(e),
309
- });
280
+ if (hostKeyRejection === undefined)
281
+ return resolve({ ok: false, error: this.connErr(e) });
282
+ resolve(hostKeyRejection.standing ? this.seat.answer(connectRefusal("host_key_rejected", hostKeyRejection.reason, e)) : { ok: false, error: new RemoteExecutionError("connect_failed", hostKeyRejection.reason, e) });
310
283
  });
311
284
  try {
312
285
  c.connect(opts);
@@ -328,6 +301,9 @@ export class RemoteSshExecutionEnv {
328
301
  async reconnect(_sessionToken) {
329
302
  if (this.destroyed)
330
303
  return { ok: false, error: new RemoteExecutionError("connect_failed", "SSH env destroyed — create a fresh env instead of reviving") };
304
+ const held = this.seat.held;
305
+ if (held)
306
+ return held;
331
307
  if (this.inFlight && this.inFlight.epoch === this.connEpoch && this.inFlight.kind === "replace")
332
308
  return this.inFlight.promise;
333
309
  this.connEpoch++;
@@ -356,7 +332,33 @@ export class RemoteSshExecutionEnv {
356
332
  fsExec(command, abortSignal) {
357
333
  return this.execCall(createCwdGuardedCall("ssh", command, undefined, undefined), { abortSignal });
358
334
  }
359
- async execCall(call, options) {
335
+ execCall(call, options) {
336
+ const outBuf = new RollingTailBuffer();
337
+ return this.runBuffered(call, options, {
338
+ push: (d) => outBuf.push(d),
339
+ tail: outBuf,
340
+ take: () => {
341
+ const out = outBuf.result();
342
+ return markTruncated(out.text, out.droppedBytes);
343
+ },
344
+ });
345
+ }
346
+ boundExec(cmd, cap, abortSignal) {
347
+ const chunks = [];
348
+ let kept = 0;
349
+ return this.runBuffered(createCwdGuardedCall("ssh", cmd, undefined, undefined), { abortSignal, timeout: this.cfg.dataTimeoutMs / 1000 }, {
350
+ push: (d) => {
351
+ if (kept > cap)
352
+ return;
353
+ const piece = d.length > cap + 1 - kept ? d.subarray(0, cap + 1 - kept) : d;
354
+ chunks.push(piece);
355
+ kept += piece.length;
356
+ },
357
+ tail: undefined,
358
+ take: () => new Uint8Array(Buffer.concat(chunks)),
359
+ });
360
+ }
361
+ async runBuffered(call, options, sink) {
360
362
  const ready = await this.ensureConnected(options?.abortSignal);
361
363
  if (!ready.ok)
362
364
  return ready;
@@ -365,7 +367,6 @@ export class RemoteSshExecutionEnv {
365
367
  return { ok: false, error: new ExecutionError("shell_unavailable", "SSH not connected — call connect() first") };
366
368
  const timeoutMs = options?.timeout != null ? options.timeout * 1000 : this.cfg.commandTimeoutMs;
367
369
  return new Promise((resolve) => {
368
- const outBuf = new RollingTailBuffer();
369
370
  const errBuf = new RollingTailBuffer();
370
371
  const recordStderr = (bytes) => {
371
372
  if (bytes.length > 0)
@@ -381,7 +382,7 @@ export class RemoteSshExecutionEnv {
381
382
  };
382
383
  const captured = () => {
383
384
  recordStderr(call.endStderr());
384
- return buildCapturedOutput(outBuf, errBuf);
385
+ return buildCapturedOutput(sink.tail, errBuf);
385
386
  };
386
387
  let settled = false;
387
388
  let streamRef;
@@ -427,7 +428,7 @@ export class RemoteSshExecutionEnv {
427
428
  }
428
429
  stream
429
430
  .on("data", (d) => {
430
- outBuf.push(d);
431
+ sink.push(d);
431
432
  options?.onStdout?.(d.toString());
432
433
  })
433
434
  .on("close", (code, signal) => {
@@ -438,9 +439,8 @@ export class RemoteSshExecutionEnv {
438
439
  const held = call.endStderr();
439
440
  const won = finish(() => {
440
441
  recordStderr(held);
441
- const out = outBuf.result();
442
442
  const err = errBuf.result();
443
- return call.settle(ok({ stdout: markTruncated(out.text, out.droppedBytes), stderr: markTruncated(err.text, err.droppedBytes), exitCode }));
443
+ return call.settle(ok({ stdout: sink.take(), stderr: markTruncated(err.text, err.droppedBytes), exitCode }));
444
444
  });
445
445
  if (won)
446
446
  emitStderr(held);
@@ -688,6 +688,7 @@ export class RemoteSshExecutionEnv {
688
688
  run: (cmd, abortSignal) => this.fsExec(cmd, abortSignal),
689
689
  resolve: (p) => this.resolve(p),
690
690
  tempDir: () => null,
691
+ boundLane: { run: (cmd, cap, abortSignal) => this.boundExec(cmd, cap, abortSignal), payload: "base64", boundSec: this.cfg.dataTimeoutMs / 1000 },
691
692
  }));
692
693
  }
693
694
  async readLink(p, abortSignal) {
@@ -2,7 +2,7 @@
2
2
  * Shared shell/path helpers for the remote `RemoteExecutionEnv` adapters (ssh / adb / k8s / local-docker, and e2b where it
3
3
  * shells out). Kept in ONE place so the security-sensitive quoting can't drift between adapters (council design/#4, #5).
4
4
  */
5
- import { ExecutionError, FileError, type FileInfo, type RemoteExecutionError, type Result, type SymlinkChain } from "@sema-agent/core";
5
+ import { ExecutionError, FileError, type BoundFileObject, type FileInfo, type RemoteExecutionError, type Result, type SymlinkChain } from "@sema-agent/core";
6
6
  import type { ChildProcess } from "node:child_process";
7
7
  /**
8
8
  * D5 pipe-destroy grace (parity with core's vendored `EXEC_FORCE_SETTLE_GRACE_MS`, harness/env/nodejs.js):
@@ -206,6 +206,103 @@ export type ShellChainRun = (cmd: string) => Promise<Result<{
206
206
  * 三条远端腿的文件面探针不带 cwd(车PL 验修 ③)⇒ 这条命令不会因为某个工作目录不可用而不跑。
207
207
  */
208
208
  export declare function runSymlinkChain(absPath: string, run: ShellChainRun): Promise<Result<SymlinkChain, FileError>>;
209
+ /**
210
+ * 绑定读脚本的退出码闭集(与 shell / 工具自己的 1 / 2 / 124–127 / 128+ 不撞)。读法只在 {@link boundReadRefusal}(穷举 switch,无 default 臂)。
211
+ * · `link` —— 预检:叶是链接(没有 `O_NOFOLLOW` 可用,先查);
212
+ * · `moved` —— 打开之后的那一次观测不在规范键上(叶 / 父段打开时是链接、打开后被改名 / 删掉),或身份 / 类型不等于开答时;
213
+ * · `kind` —— 既非普通文件也非目录(预检或打开之后);
214
+ * · `over` —— 这一答量到的大小超过 `max`(先量后读:超了一个字节都不读);
215
+ * · `platform` —— 给不出那一次观测或缺工具(见上段)⇒ `not_supported`;
216
+ * · `name` —— 列目录时有一个名字带换行:一行一条的答装不下它而不生歧义(名字里的换行能伪造出一条假条目),整答拒、不列一半。
217
+ */
218
+ export declare const BOUND_READ_EXITS: {
219
+ readonly link: 90;
220
+ readonly moved: 91;
221
+ readonly kind: 92;
222
+ readonly over: 93;
223
+ readonly platform: 94;
224
+ readonly name: 95;
225
+ };
226
+ export type BoundReadExit = keyof typeof BOUND_READ_EXITS;
227
+ /** 一答的操作闭集(工厂的四个成员各一答;`open` 是 `openBound` 那一答)。 */
228
+ export type BoundReadOp = "open" | "size" | "readBytes" | "list";
229
+ /**
230
+ * 绑定读的 POSIX sh 脚本 —— **常量**(不拼任何值;路径与参数经位置参数 `$1…$6` 进来):五条远端腿发的是逐字节同一串。
231
+ * 位置参数:`$1` 规范键 · `$2` 操作 · `$3` 开答时的身份 · `$4` 开答时的类型 · `$5` `max` · `$6` 载荷编码(`raw` / `base64`)。
232
+ *
233
+ * 步骤(每一答都整走一遍):
234
+ * ① 平台与工具:`/proc/self/fd` 不在、或缺工具 ⇒ `platform`,**在打开任何东西之前**;
235
+ * ② 预检:叶是链接 ⇒ `link`;在场却既非普通文件也非目录(FIFO / 设备 / 套接字)⇒ `kind` —— 预检只缩小窗口(shell 没有 `O_NOFOLLOW` / `O_NONBLOCK`),关不上它;
236
+ * ③ 一次打开、整条答共用:复合命令重定向 `{ …; } < "$f"`(**不用** `exec 3<`:ksh93 给 `exec` 开的 > 2 号描述符置 close-on-exec,子进程读不到它;
237
+ * 复合命令重定向的 0 号描述符照常继承 —— 外部命令、命令替换、shell 自己都看见同一只打开);打开失败 ⇒ shell 自己的报错 + 退 1(读法归 stderr 分类);
238
+ * ④ 那一次观测:`readlink /proc/self/fd/0`(内核给出「这只打开的对象此刻叫什么」;`/proc/self` 在外部命令里就是它自己,它继承的 0 号描述符就是这次打开)
239
+ * 须逐字等于规范键,否则 `moved` —— 命令替换会吞掉**全部**尾随换行,所以读数后面垫一个哨兵字符、只剥 `readlink` 自己那一个换行(名字以换行结尾的对象
240
+ * 与规范键不等);身份与类型 `stat -L -c '%d:%i %s %F' /proc/self/fd/0`(跟的是魔法链接:这只打开的对象本身,不是按路径再查;`%F` 是人读的词,
241
+ * GNU 按区域设置翻译 ⇒ 整只脚本在 `LC_ALL=C` 下跑,工具的报错句也因此是共享分类器认得的英文);
242
+ * 类型须是普通文件或目录,否则 `kind`;带了开答身份的答(size / read / list)⇒ 身份**与类型**须都等于开答时,否则 `moved`;
243
+ * ⑤ 作答,只经这一次打开:`open` 一行 `<身份> <file|directory>`;`size` 一行十进制;`readBytes` 先量(> `max` ⇒ 打出量到的大小、`over`,一个字节不读),
244
+ * 一行十进制大小 + 载荷 —— 大小 > 0 读恰好那么多(与 core 读口同读:开读那一刻的大小),= 0(procfs 一类)读到流尽、至多 `max + 1`;
245
+ * base64 腿的 `head | base64` 管道里 `head` 自己的退出码经 4 号描述符带出来(复合命令重定向,不用 `exec`):读失败 ⇒ 退 1,不交 `base64` 的 0;
246
+ * `list` 经 `cd -P /proc/self/fd/0` 进**这只打开的目录**,逐条 lstat 语义一行 `<十六进制模式> <大小> <mtime 秒> <名>`;名字带换行 ⇒ `name`。
247
+ * 全程不用 `!`(某些登录 shell 的历史展开)、不用 `pipefail`(dash 没有);读的截短另由读法按大小对拍兜住({@link createShellRebindingAnswers})。
248
+ */
249
+ export declare const BOUND_READ_SCRIPT: string;
250
+ /** 一答的入参(开答之后的三答带开答时的身份与类型)。 */
251
+ export interface BoundReadArgs {
252
+ readonly bound?: BoundPlacement;
253
+ readonly max?: number;
254
+ }
255
+ /**
256
+ * 一答发给目标的整串(**纯数据** ⇒ `build*`):先判 `timeout(1)` 在不在(不在 ⇒ `platform`,有界给不出就不答),再以 `timeout <boundSec 向下取整,至少 1> sh -c <脚本>` 跑 ——
257
+ * 显式套一层 `sh`,把方言钉在目标的 POSIX `/bin/sh` 上(容器的命令 shell 可能是 bash、e2b 起的是登录 bash、ssh 交给远端登录 shell)。
258
+ * 整条包在一个 `{ …; }` 里,只有一个退出状态(与 {@link buildSymlinkChainCommand} 同一条理由)。值全经 {@link shellQuote} 进位置参数,脚本串里不拼值。
259
+ */
260
+ export declare function buildBoundReadCommand(abs: string, op: BoundReadOp, args: BoundReadArgs, payload: BoundReadLane["payload"], boundSec: number): string;
261
+ /**
262
+ * 一条远端腿交给绑定读的那一条传输(各腿只造这一只闭包;读法与归码不在腿上)。
263
+ * 🔴 字节回程律:`stdout` **逐字节**(任何丢弃 / 截尾的收集器都不许接在这里 —— ssh 的命令面收集器是 8 MiB 滚动尾,所以 ssh 另造一只按本答上界封顶的);
264
+ * 收集器有上界的腿保留至多 `stdoutCap + 1` 字节,读法见到比上界长的答即拒(不解码一段尾巴)。
265
+ */
266
+ export interface BoundReadLane {
267
+ run(cmd: string, stdoutCap: number, abortSignal?: AbortSignal): Promise<Result<{
268
+ stdout: Uint8Array;
269
+ stderr: string;
270
+ exitCode: number;
271
+ }, ExecutionError | RemoteExecutionError>>;
272
+ /** 载荷在这条腿的 stdout 上怎么走:`raw`(二进制收集器:local-docker / adb)或 `base64`(文本面:k8s 的 WS 文本帧 / e2b SDK 的字符串 / ssh)。 */
273
+ readonly payload: "raw" | "base64";
274
+ /** 一答在目标上的墙钟(整秒,`timeout(1)`)= 该腿的数据界。 */
275
+ readonly boundSec: number;
276
+ }
277
+ /** 开答定下的那只对象(工厂与答者之间的唯一凭据):身份 `<dev>:<inode>` + 类型。 */
278
+ export interface BoundPlacement {
279
+ readonly identity: string;
280
+ readonly kind: "file" | "directory";
281
+ }
282
+ /** 一只目录条目(没有 `path`:工厂把它锚在规范键之下,答者给的拼法不作数)。 */
283
+ export type BoundListEntry = Omit<FileInfo, "path">;
284
+ /**
285
+ * 逐答重绑的答者(传输那一半):每一答都是一次完整的重绑 —— 打开、核对它就是 `bound`、经这一次打开作答。shell 腿 = {@link createShellRebindingAnswers};
286
+ * 设备腿 = 一条 `openBound` 协议指令。
287
+ */
288
+ export interface RebindingAnswers {
289
+ open(abortSignal?: AbortSignal): Promise<Result<BoundPlacement, FileError>>;
290
+ size(bound: BoundPlacement): Promise<Result<number, FileError>>;
291
+ readBytes(bound: BoundPlacement, max: number, abortSignal?: AbortSignal): Promise<Result<Uint8Array, FileError>>;
292
+ list(bound: BoundPlacement, abortSignal?: AbortSignal): Promise<Result<BoundListEntry[], FileError>>;
293
+ }
294
+ /** 五条远端腿的答者:四个成员各一答,每一答 = {@link BOUND_READ_SCRIPT} 的一次往返(`createShellRebindingAnswers` 返回活对象 ⇒ `create*`)。 */
295
+ export declare function createShellRebindingAnswers(abs: string, lane: BoundReadLane): RebindingAnswers;
296
+ /**
297
+ * 逐答重绑的 `BoundFileObject`(活对象 ⇒ `create*`):每个成员一答;`close()` 不发往返(远端什么都不持有)。
298
+ * 工厂自己的三道闸(不信任答者):读的不是文件 / 列的不是目录 ⇒ 不发往返、照 core 句拒;`max` 不是非负安全整数 ⇒ `invalid`;
299
+ * 答回的字节比 `max` 多 ⇒ `invalid`(二道闸,同 {@link symlinkChainHopsFromStdout} 的「再切一次」);条目名不是单段 ⇒ 整答拒,条目锚在 `abs` 之下(执行缝路径律)。
300
+ */
301
+ export declare function createRebindingBoundFile(abs: string, bound: BoundPlacement, answers: RebindingAnswers): BoundFileObject;
302
+ /** `openBound` 的整只半场(开答 → 工厂)。答者给的身份与类型就是工厂之后每一答对拍的凭据。 */
303
+ export declare function openRebinding(abs: string, answers: RebindingAnswers, abortSignal?: AbortSignal): Promise<Result<BoundFileObject, FileError>>;
304
+ /** 远端腿 `openBound` 的一行调用(经基座的腿与 e2b 直调;本版 ssh 缓装、不暴露):同一只脚本、同一只读法、同一只工厂。 */
305
+ export declare function openBoundOverShell(abs: string, lane: BoundReadLane, abortSignal?: AbortSignal): Promise<Result<BoundFileObject, FileError>>;
209
306
  /**
210
307
  * 脚本删一只私有目录没删掉时写到 **stdout** 的那一条记录的头:`<头> <目录路径>\0`(NUL 收尾 —— 路径里可以有换行)。
211
308
  * 残留记录走 stdout、错误走 stderr:两条通道不混,分类器读到的 stderr 里没有路径文本。记录不改脚本的退出码:写照它自己的结局答,残留另行留痕。