@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
@@ -1,4 +1,4 @@
1
- import { ExecutionError, FileError, RemoteExecutionError, type ExecutionEnv, type ExecutionEnvFactory, type ExecStreamOptions, type FileInfo, type OutputChunk, type RemoteConnectConfig, type RemoteExecutionEnv, type Result, type SessionToken, type SnapshotId, type VmLifecycleOptions, type WorkspaceHandle, type WriteExpectation, type WriteReceipt } from "@sema-agent/core";
1
+ import { ExecutionError, FileError, RemoteExecutionError, type ExecutionEnv, type ExecutionEnvFactory, type ExecStreamOptions, type FileInfo, type OutputChunk, type RemoteConnectConfig, type RemoteExecutionEnv, type Result, type SessionToken, type SnapshotId, type VmLifecycleOptions, type WorkspaceHandle, type WriteExpectation, type WriteReceipt, type BoundFileObject } from "@sema-agent/core";
2
2
  import { type DeviceOwner, type DeviceRejectCode, type DeviceRow, type DeviceStore } from "../device-store.js";
3
3
  import type { DeviceEnrollment } from "../device-enrollment.js";
4
4
  import { type ExecutionPlacementSeat } from "../execution-placement.js";
@@ -48,6 +48,13 @@ type RejectCode = DeviceRejectCode;
48
48
  */
49
49
  export declare const DEVICE_REJECT_TO_EXEC_ERROR: Readonly<Record<RejectCode, ExecutionError["code"]>>;
50
50
  export declare const DEVICE_REJECT_TO_FILE_ERROR: Readonly<Record<RejectCode, FileError["code"]>>;
51
+ /**
52
+ * 生命周期面(connect/reconnect)的 typed 拒 → `RemoteExecutionErrorCode`。
53
+ *
54
+ * `connect_failed` = 本次到不了(**可重试**,core `RETRYABLE_REMOTE_ERROR_CODES` 含它);
55
+ * `auth_failed` = 永久拒(NEVER retry —— 换绑/吊销/不是你的设备,重试一万次也是同一答)。
56
+ */
57
+ export declare const DEVICE_REJECT_TO_REMOTE_ERROR: Readonly<Record<RejectCode, RemoteExecutionError["code"]>>;
51
58
  export interface DeviceEnvLogger {
52
59
  warn(msg: string, fields?: Record<string, unknown>): void;
53
60
  info?(msg: string, fields?: Record<string, unknown>): void;
@@ -102,9 +109,15 @@ export declare class RemoteDeviceExecutionEnv implements RemoteExecutionEnv {
102
109
  readLink?: (path: string, abortSignal?: AbortSignal) => Promise<Result<string, FileError>>;
103
110
  writeFileExclusive?: (path: string, content: string | Uint8Array, abortSignal?: AbortSignal) => Promise<Result<void, FileError>>;
104
111
  writeFileGuarded?: (path: string, content: string | Uint8Array, expect: WriteExpectation, abortSignal?: AbortSignal) => Promise<Result<WriteReceipt, FileError>>;
112
+ openBound?: (path: string, abortSignal?: AbortSignal) => Promise<Result<BoundFileObject, FileError>>;
105
113
  private connected;
106
114
  /** 在飞的那一次准入(单飞记忆)——见 {@link admitShared}。settle 即清,**不缓存败诺**。 */
107
115
  private connectPromise?;
116
+ /**
117
+ * 建连座(`remote-env-connect-seat.ts`):`connect` 的唯一次序 —— 准入在飞 ⇒ 等那一次(调用方的取消只退它自己的等待)· 已连 ⇒ 当下的把手、不再准入 · 否则准入一次。
118
+ * 本车道不记拒:准入每次都是对绑定 / 吊销 / 在线的一次新读(拒码按生命周期表拼:{@link DEVICE_REJECT_TO_REMOTE_ERROR})。
119
+ */
120
+ private readonly seat;
108
121
  /** 拆除是**终态**(六条兄弟腿同纪律:destroy 之后 connect/exec/读面一律 fail-closed,不复活)。 */
109
122
  private destroyed;
110
123
  constructor(cfg: DeviceEnvConfig);
@@ -170,10 +183,8 @@ export declare class RemoteDeviceExecutionEnv implements RemoteExecutionEnv {
170
183
  /**
171
184
  * 懒连接:**首次调用任一面**(exec / execStream / 文件面)时自动走一次准入。
172
185
  *
173
- * 🔴 为什么这条必须在 adapter 里(而不是「谁用谁先 connect」):task/run 车道**没有任何人**会替
174
- * adapter 调 `connect()` —— 工厂铸出 env 就直接用它的 exec/文件面(本仓亲核:`src/` 里对执行环境的
175
- * `.connect()` 只有 bench 与生命周期面自己)。所以一条「设了未连闸、又不自连」的腿,它的每一次工具
176
- * 调用都被自家的闸挡回,而设备侧一条指令帧都收不到。e2b(首用建 VM)/ k8s(`connectPromise ??=`)/
186
+ * 🔴 为什么这条必须在 adapter 里(而不是「谁用谁先 connect」):面可能在任何人连它之前就被调到(工厂交出之前的附件物化层、
187
+ * 委派子代的放置检查)。所以一条「设了未连闸、又不自连」的腿,它的每一次工具调用都被自家的闸挡回,而设备侧一条指令帧都收不到。e2b(首用建 VM)/ k8s(`connectPromise ??=`)/
177
188
  * ssh(single-flight join)/ local-docker 四腿早就是懒连接;本腿从此同形,`connect()` 的语义
178
189
  * (准入 + 取活连接 + 保护面对拍)一个字不动,只是触发时机与兄弟一致。
179
190
  *
@@ -262,6 +273,12 @@ export declare class RemoteDeviceExecutionEnv implements RemoteExecutionEnv {
262
273
  canonicalPath(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
263
274
  fileInfo(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo, FileError>>;
264
275
  private doReadLink;
276
+ private doOpenBound;
277
+ /**
278
+ * 逐答重绑的答者:每一答一条 `openBound` 指令,结局 kind **按 op 精确对拍**(`open` / `size` / `list` ⇒ `stat`,`readBytes` ⇒ `read`;答错 = 协议违约,不猜)。
279
+ * 开答之后的三答带开答时的 `identity` + `kind`,执行器在本机核对这一次打开的对象与它们都相等;每一答前照文件面同一道门(已拆 ⇒ 拒;首用准入)。
280
+ */
281
+ private boundAnswers;
265
282
  readBinaryFile(p: string, abortSignal?: AbortSignal): Promise<Result<Uint8Array, FileError>>;
266
283
  readTextFile(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
267
284
  readTextLines(p: string, options?: {
@@ -1,10 +1,12 @@
1
1
  import { posix } from "node:path";
2
2
  import { anchoredPath, anchorUnder, joinedPath } from "./exec-path-law.js";
3
3
  import { ExecutionError, FileError, RemoteExecutionError, } from "@sema-agent/core";
4
+ import { openRebinding } from "./remote-shell.js";
4
5
  import { capabilitiesFor } from "../execution-lane-caps.js";
5
6
  import { DEVICE_ERROR_CODES, isDeviceAbsolutePath, } from "../device-store.js";
6
7
  import { boundDeviceOf } from "../execution-placement.js";
7
8
  import { createCutError, createExecFailure } from "../exec-cut.js";
9
+ import { createConnectSeat, withdrawOnAbort } from "./remote-env-connect-seat.js";
8
10
  export const DEVICE_PROVIDER = "device";
9
11
  const ok = (value) => ({ ok: true, value });
10
12
  const STREAM_PENDING_MAX_BYTES = 2 * 1024 * 1024;
@@ -89,12 +91,12 @@ export const DEVICE_REJECT_TO_FILE_ERROR = {
89
91
  "limit.rate_exceeded": "target_unavailable",
90
92
  "draining": "target_unavailable",
91
93
  };
92
- const DEVICE_REJECT_TO_REMOTE_ERROR = {
94
+ export const DEVICE_REJECT_TO_REMOTE_ERROR = {
93
95
  "device.not_bound": "connect_failed",
94
96
  "device.offline": "connect_failed",
95
97
  "device.busy": "connect_failed",
96
98
  "device.attached_elsewhere": "connect_failed",
97
- "device.claim_conflict": "auth_failed",
99
+ "device.claim_conflict": "connect_failed",
98
100
  "device.provider_mismatch": "auth_failed",
99
101
  "device.identity_mismatch": "auth_failed",
100
102
  "device.revoked": "auth_failed",
@@ -110,6 +112,9 @@ function rejectText(reject) {
110
112
  }
111
113
  const ENV_TERMINAL = { kind: "env_terminal" };
112
114
  const ENV_TERMINAL_MESSAGE = "execution env already destroyed";
115
+ function connectAnswerOf(admitted) {
116
+ return admitted.ok ? { ok: true, value: admitted.value } : { ok: false, error: new RemoteExecutionError(admitted.failure.remote, admitted.failure.message) };
117
+ }
113
118
  function failureFor(code, message) {
114
119
  return { remote: DEVICE_REJECT_TO_REMOTE_ERROR[code], exec: DEVICE_REJECT_TO_EXEC_ERROR[code], file: DEVICE_REJECT_TO_FILE_ERROR[code], message };
115
120
  }
@@ -137,8 +142,14 @@ export class RemoteDeviceExecutionEnv {
137
142
  readLink;
138
143
  writeFileExclusive;
139
144
  writeFileGuarded;
145
+ openBound;
140
146
  connected = false;
141
147
  connectPromise;
148
+ seat = createConnectSeat({
149
+ live: () => (this.connected && !this.destroyed ? this.workspaceHandle() : undefined),
150
+ inFlight: () => this.connectPromise?.then(connectAnswerOf),
151
+ establish: (config) => withdrawOnAbort(this.admitShared().then(connectAnswerOf), config?.abortSignal),
152
+ });
142
153
  destroyed = false;
143
154
  constructor(cfg) {
144
155
  this.cfg = cfg;
@@ -150,6 +161,8 @@ export class RemoteDeviceExecutionEnv {
150
161
  this.writeFileExclusive = (p, c, s) => this.doWrite("writeFileExclusive", p, c, undefined, s);
151
162
  if (cfg.supports?.has("writeFileGuarded") === true)
152
163
  this.writeFileGuarded = (p, c, e, s) => this.doGuardedWrite(p, c, e, s);
164
+ if (cfg.supports?.has("openBound") === true)
165
+ this.openBound = (p, s) => this.doOpenBound(p, s);
153
166
  }
154
167
  guardFaceSnapshot() {
155
168
  const out = new Set();
@@ -196,12 +209,7 @@ export class RemoteDeviceExecutionEnv {
196
209
  };
197
210
  }
198
211
  async connect(config) {
199
- if (config?.abortSignal?.aborted === true)
200
- return { ok: false, error: new RemoteExecutionError("aborted", "connect aborted") };
201
- const admitted = await this.admitShared();
202
- if (admitted.ok)
203
- return ok(admitted.value);
204
- return { ok: false, error: new RemoteExecutionError(admitted.failure.remote, admitted.failure.message) };
212
+ return this.seat.connect(config);
205
213
  }
206
214
  async admitShared() {
207
215
  const inflight = this.connectPromise;
@@ -577,6 +585,96 @@ export class RemoteDeviceExecutionEnv {
577
585
  return ok(o.linkTarget);
578
586
  });
579
587
  }
588
+ async doOpenBound(p, abortSignal) {
589
+ const gated = await this.fsGate(p);
590
+ if (gated)
591
+ return gated;
592
+ const abs = this.resolve(p);
593
+ return openRebinding(abs, this.boundAnswers(abs), abortSignal);
594
+ }
595
+ boundAnswers(abs) {
596
+ const send = async (args, signal, onChunk) => {
597
+ const gated = await this.fsGate(abs);
598
+ if (gated)
599
+ return gated;
600
+ return this.dispatch("openBound", { path: abs, ...args }, { timeoutMs: this.cfg.execTimeoutMs, ...(signal === undefined ? {} : { signal }), ...(onChunk === undefined ? {} : { onChunk }) });
601
+ };
602
+ const violation = (what) => ({ ok: false, error: new FileError("unknown", `device returned an openBound outcome ${what} — protocol violation`, abs) });
603
+ return {
604
+ open: async (signal) => {
605
+ const res = await send({ op: "open" }, signal);
606
+ if ("ok" in res)
607
+ return res;
608
+ return this.fileOutcome(res, abs, (o) => {
609
+ if (o.kind !== "stat")
610
+ return this.wrongOutcome("stat", o.kind, abs);
611
+ if (typeof o.identity !== "string" || o.identity === "" || o.info === undefined)
612
+ return violation("without identity and info for the open answer");
613
+ if (o.info.kind !== "file" && o.info.kind !== "dir")
614
+ return violation(`naming a ${o.info.kind} (an executor refuses anything but a regular file or a directory with invalid)`);
615
+ return ok({ identity: o.identity, kind: o.info.kind === "dir" ? "directory" : "file" });
616
+ });
617
+ },
618
+ size: async (bound) => {
619
+ const res = await send({ op: "size", identity: bound.identity, kind: bound.kind }, undefined);
620
+ if ("ok" in res)
621
+ return res;
622
+ return this.fileOutcome(res, abs, (o) => {
623
+ if (o.kind !== "stat")
624
+ return this.wrongOutcome("stat", o.kind, abs);
625
+ if (o.info === undefined)
626
+ return violation("without info for the size answer");
627
+ return ok(o.info.size);
628
+ });
629
+ },
630
+ readBytes: async (bound, max, signal) => {
631
+ const parts = [];
632
+ let got = 0;
633
+ let stray;
634
+ const res = await send({ op: "readBytes", identity: bound.identity, kind: bound.kind, max }, signal, (c) => {
635
+ if (c.kind !== "data") {
636
+ stray ??= c.kind;
637
+ return;
638
+ }
639
+ if (c.data === undefined || c.data.length === 0)
640
+ return;
641
+ got += c.data.length;
642
+ if (got <= max)
643
+ parts.push(c.data);
644
+ });
645
+ if ("ok" in res)
646
+ return res;
647
+ return this.fileOutcome(res, abs, (o) => {
648
+ if (o.kind !== "read")
649
+ return this.wrongOutcome("read", o.kind, abs);
650
+ if (stray !== undefined)
651
+ return violation(`carrying a ${stray} chunk for a readBytes answer (only data chunks belong to it)`);
652
+ if (got > max)
653
+ return { ok: false, error: new FileError("invalid", `The held file is over the ${max}-byte read cap (${got} bytes or more arrived); it was not read`, abs) };
654
+ return ok(new Uint8Array(Buffer.concat(parts)));
655
+ });
656
+ },
657
+ list: async (bound, signal) => {
658
+ const res = await send({ op: "list", identity: bound.identity, kind: bound.kind }, signal);
659
+ if ("ok" in res)
660
+ return res;
661
+ return this.fileOutcome(res, abs, (o) => {
662
+ if (o.kind !== "stat")
663
+ return this.wrongOutcome("stat", o.kind, abs);
664
+ if (o.entries === undefined)
665
+ return violation("without entries for the list answer");
666
+ const entries = [];
667
+ for (const w of o.entries) {
668
+ const kind = w.kind === "dir" ? "directory" : w.kind === "symlink" ? "symlink" : w.kind === "file" ? "file" : undefined;
669
+ if (kind === undefined)
670
+ continue;
671
+ entries.push({ name: posix.basename(w.path), kind, size: w.size, mtimeMs: w.mtimeMs });
672
+ }
673
+ return ok(entries);
674
+ });
675
+ },
676
+ };
677
+ }
580
678
  async readBinaryFile(p, abortSignal) {
581
679
  const gated = await this.fsGate(p);
582
680
  if (gated)
@@ -20,7 +20,7 @@
20
20
  * iterator MAY throw `RemoteExecutionError` mid-stream before an `exit` chunk (seam contract, OutputChunk).
21
21
  */
22
22
  import { Sandbox, type SandboxOpts, type SandboxConnectOpts } from "e2b";
23
- import { FileError, ExecutionError, RemoteExecutionError, type ExecutionEnv, type ExecResult, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type SymlinkChain, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type ExecutionEnvFactory, type VmLifecycleOptions, type BackgroundShellCapability, type BackgroundShellId, type BackgroundShellError, type BackgroundSpawnOptions, type BackgroundPoll, type ExecutionEnvCapabilities } from "@sema-agent/core";
23
+ import { FileError, ExecutionError, RemoteExecutionError, type ExecutionEnv, type ExecResult, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type SymlinkChain, type BoundFileObject, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type ExecutionEnvFactory, type VmLifecycleOptions, type BackgroundShellCapability, type BackgroundShellId, type BackgroundShellError, type BackgroundSpawnOptions, type BackgroundPoll, type ExecutionEnvCapabilities } from "@sema-agent/core";
24
24
  import { type SandboxReadFloorKnob } from "../execution-lane-caps.js";
25
25
  import { type SandboxReadFloor } from "./sandbox-read-floor.js";
26
26
  import { type LaneHomeConfig } from "../lane-home-probe.js";
@@ -127,9 +127,12 @@ export declare class RemoteContainerExecutionEnv implements RemoteExecutionEnv,
127
127
  private destroyed;
128
128
  /** 家目录的座(主张在场 ⇒ 每只沙箱对证 · 否则建沙箱那一拍实测 · 实测缺席 ⇒ 缺席 + 一行 warn)。 */
129
129
  private readonly homeSeat;
130
- /** 这只环境的拒 —— 记一次、此后每次连接 / 恢复原样回它(同一模板同一个答案:不再建沙箱、不再问)。两个写者:家目录(沙箱答了而与主张不一致,或答的不是绝对路径)
131
- * 与恢复点的填充(换一台沙箱也是同一个答的拒:种子不是项目包、摘要不符、镜像没有 `tar`、镜像预置撞名 —— `FillOutcome.standing`)。 */
132
- private connectRefusal?;
130
+ /**
131
+ * 建连座(`remote-env-connect-seat.ts`):`connect` 的唯一次序(记住的拒 ⇒ 原样再答 · 一走在飞 ⇒ 等它 · 已连 ⇒ 当下的把手 · 否则建一只沙箱),并持有这只环境记住的拒 ——
132
+ * 记一次、此后每次连接 / 恢复原样回它(同一模板同一个答案:不再建沙箱、不再问)。两件立定的事实写它:家目录(沙箱答了而与主张不一致,或答的不是绝对路径;`env.home_unconfirmed`)
133
+ * 与恢复点的填充(换一台沙箱也是同一个答的拒:种子不是项目包、摘要不符、镜像没有 `tar`、镜像预置撞名 —— `FillOutcome.standing`;`env.sandbox_refused`)。
134
+ */
135
+ private readonly seat;
133
136
  /** In-flight connect/resume, memoized so concurrent lazy first-use OR resume provisions exactly one VM (no double-bill). `target` = the snapshot a resume
134
137
  * restores (`undefined` = a fresh sandbox): {@link resumeVM} shares only a resume of the SAME snapshot — a connect for anything else is not the
135
138
  * workspace it is asked to restore (it waits that one out, then releases the sandbox it left). */
@@ -223,8 +226,9 @@ export declare class RemoteContainerExecutionEnv implements RemoteExecutionEnv,
223
226
  }>;
224
227
  /**
225
228
  * 家目录:座要问({@link LaneHomeSeat.asks}:主张在场 ⇒ 对证;没主张 ⇒ 量到绝对路径为止)⇒ 在这只沙箱里问一次 `$HOME`(前台 `commands.run`,与命令同一个
226
- * 模板缺省用户、同一份沙箱 env),读数交座。返回这只沙箱的拒(`connect_failed`,拒句 = 同一只判官的句子),或 `undefined`(照用)。
227
- * 沙箱答了的拒记在环境上(同一模板同一个答案);没答上(非零退出 / RPC 失败 / 输出里没有标记行)⇒ 这一次不开、不记,下一次重问;调用方中止 ⇒ `aborted`、不交座。
229
+ * 模板缺省用户、同一份沙箱 env),读数交座。返回这只沙箱的拒(拒句 = 同一只判官的句子),或 `undefined`(照用)。
230
+ * 沙箱答了的拒是立定的拒(`env.home_unconfirmed`,建连座记住它:同一模板同一个答案);没答上(非零退出 / RPC 失败 / 输出里没有标记行)⇒ 这一次不开、不记(`connect_failed`),
231
+ * 下一次重问;调用方中止 ⇒ `aborted`、不交座。
228
232
  * 上界 = 控制面 RPC 的墙钟(`rpcTimeoutMs`:问一个环境变量,不是跑用户命令)。
229
233
  */
230
234
  private homeVerdict;
@@ -295,6 +299,11 @@ export declare class RemoteContainerExecutionEnv implements RemoteExecutionEnv,
295
299
  * 🔴 exit≠0 归 `unknown` 而不是 `not_found`(与本腿 `readLink` 那一格刻意分家,同 `posix-shell-fs` 的理由):
296
300
  * 「叶不是软链」与「叶不存在」在这条命令上都是 exit 0 + 空 stdout,所以 exit≠0 一定不是「文件不在」。 */
297
301
  canonicalChain(p: string, abortSignal?: AbortSignal): Promise<Result<SymlinkChain, FileError>>;
302
+ /** 绑定读(逐答重绑,Linux 读法)。本腿不并进 `posix-shell-fs` 基座(原生 SDK 腿),但脚本、读法与工厂与四条 shell 腿**是同一只**
303
+ * (`remote-shell.ts` `openBoundOverShell`),这里只多一条传输:{@link execInternal}(适配器自己的固定命令那一起动,与链脚本同一形)、数据界。
304
+ * 🔴 字节只经 stdout 的 base64 回来 —— `files.read` 按路径读,不能用;也不「拷进暂存件再按路径读」(同身份进程可换暂存件或它的祖先目录)。 */
305
+ openBound(p: string, abortSignal?: AbortSignal): Promise<Result<BoundFileObject, FileError>>;
306
+ private get boundLane();
298
307
  canonicalPath(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
299
308
  exists(p: string, abortSignal?: AbortSignal): Promise<Result<boolean, FileError>>;
300
309
  createDir(p: string, options?: {
@@ -7,7 +7,8 @@ import { DEPLOYMENT_SHAPE_PREDICATES } from "../deployment-shape.js";
7
7
  import { ADAPTER_START, BOOTSTRAP_START, createSandboxReadFloor, e2bFloorCommand, engineStart, } from "./sandbox-read-floor.js";
8
8
  import { LANE_HOME_PROBE_COMMAND, LaneHomeSeat } from "../lane-home-probe.js";
9
9
  import { fileErrorFromExec } from "./remote-env-file-error.js";
10
- import { appendFileStaged, readyUpload, rewriteAppend, numEnvOr, runSymlinkChain, shellQuote, writeFileStaged } from "./remote-shell.js";
10
+ import { connectRefusal, createConnectSeat } from "./remote-env-connect-seat.js";
11
+ import { appendFileStaged, openBoundOverShell, readyUpload, rewriteAppend, numEnvOr, runSymlinkChain, shellQuote, writeFileStaged } from "./remote-shell.js";
11
12
  import { buildLivenessCut, commandBoundsOf, firstBoundOf, laneLivenessOf, LIVENESS_MS_DEFAULT } from "../exec-bounds.js";
12
13
  import { createCutError } from "../exec-cut.js";
13
14
  import { BackgroundShellManager } from "./background-shell-support.js";
@@ -37,7 +38,22 @@ export class RemoteContainerExecutionEnv {
37
38
  handle;
38
39
  destroyed = false;
39
40
  homeSeat;
40
- connectRefusal;
41
+ seat = createConnectSeat({
42
+ live: () => (this.sbx !== undefined && !this.paused ? this.workspaceHandle() : undefined),
43
+ inFlight: () => this.connecting?.promise,
44
+ establish: (config) => {
45
+ if (this.paused)
46
+ return Promise.resolve({ ok: false, error: new RemoteExecutionError("connect_failed", "execution env is suspended (paused) — resumeVM(snapshotId) before use") });
47
+ const mine = { target: undefined, promise: this.doConnect(config) };
48
+ this.connecting = mine;
49
+ const settled = () => {
50
+ if (this.connecting === mine)
51
+ this.connecting = undefined;
52
+ };
53
+ void mine.promise.then(settled, settled);
54
+ return mine.promise;
55
+ },
56
+ });
41
57
  connecting;
42
58
  activeCommands = 0;
43
59
  paused = false;
@@ -156,20 +172,7 @@ export class RemoteContainerExecutionEnv {
156
172
  }
157
173
  if (this.destroyed)
158
174
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env already destroyed") };
159
- if (this.connectRefusal)
160
- return { ok: false, error: this.connectRefusal };
161
- if (this.paused)
162
- return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env is suspended (paused) — resumeVM(snapshotId) before use") };
163
- if (this.sbx)
164
- return ok(this.workspaceHandle());
165
- if (this.connecting)
166
- return this.connecting.promise;
167
- const mine = { target: undefined, promise: this.doConnect(config) };
168
- this.connecting = mine;
169
- const r = await mine.promise;
170
- if (!r.ok && this.connecting === mine)
171
- this.connecting = undefined;
172
- return r;
175
+ return this.seat.connect(config);
173
176
  }
174
177
  async doConnect(config) {
175
178
  try {
@@ -200,9 +203,7 @@ export class RemoteContainerExecutionEnv {
200
203
  if (!filled.ok || this.destroyed) {
201
204
  await sbx.kill().catch(() => { });
202
205
  this.cfg.metrics?.inc("remote_env_connect_total", { provider: PROVIDER, result: "error" });
203
- if (!filled.ok && filled.standing)
204
- this.connectRefusal = filled.error;
205
- return { ok: false, error: filled.ok ? new RemoteExecutionError("connect_failed", "execution env destroyed during connect") : filled.error };
206
+ return filled.ok ? { ok: false, error: new RemoteExecutionError("connect_failed", "execution env destroyed during connect") } : this.seat.answer(filled);
206
207
  }
207
208
  this.sbx = sbx;
208
209
  this.paused = false;
@@ -256,8 +257,9 @@ export class RemoteContainerExecutionEnv {
256
257
  resumeVM(snapshotId, options) {
257
258
  if (this.destroyed)
258
259
  return Promise.resolve({ ok: false, error: new RemoteExecutionError("connect_failed", "env already destroyed") });
259
- if (this.connectRefusal)
260
- return Promise.resolve({ ok: false, error: this.connectRefusal });
260
+ const held = this.seat.held;
261
+ if (held)
262
+ return Promise.resolve(held);
261
263
  if (this.sbx && !this.paused && this.sbx.sandboxId === snapshotId)
262
264
  return Promise.resolve(ok(this.workspaceHandle()));
263
265
  const inFlight = this.connecting;
@@ -269,10 +271,7 @@ export class RemoteContainerExecutionEnv {
269
271
  if (this.connecting === mine)
270
272
  this.connecting = undefined;
271
273
  };
272
- mine.promise.then((r) => {
273
- if (!r.ok)
274
- forget();
275
- }, forget);
274
+ mine.promise.then(forget, forget);
276
275
  return mine.promise;
277
276
  }
278
277
  async resumeWalk(inFlight, snapshotId, options) {
@@ -280,6 +279,9 @@ export class RemoteContainerExecutionEnv {
280
279
  await inFlight.promise;
281
280
  if (this.destroyed)
282
281
  return { ok: false, error: new RemoteExecutionError("connect_failed", "env already destroyed") };
282
+ const held = this.seat.held;
283
+ if (held)
284
+ return held;
283
285
  if (this.sbx && !this.paused && this.sbx.sandboxId === snapshotId)
284
286
  return ok(this.workspaceHandle());
285
287
  }
@@ -329,9 +331,7 @@ export class RemoteContainerExecutionEnv {
329
331
  if (!filled.ok) {
330
332
  await this.rpc("sandbox.pause", (signal) => sbx.pause({ signal }), undefined, this.cfg.dataTimeoutMs).catch((e) => this.cfg.logger?.warn?.("remote_env_fill_refused_repause_failed", { sandboxId: sbx.sandboxId, error: errMsg(e) }));
331
333
  this.cfg.metrics?.inc("remote_env_resume_total", { provider: PROVIDER, result: "error" });
332
- if (filled.standing)
333
- this.connectRefusal = filled.error;
334
- return { ok: false, error: filled.error };
334
+ return this.seat.answer(filled);
335
335
  }
336
336
  this.sbx = sbx;
337
337
  this.paused = false;
@@ -383,10 +383,7 @@ export class RemoteContainerExecutionEnv {
383
383
  const verdict = this.homeSeat.settle(probe);
384
384
  if (verdict === undefined)
385
385
  return undefined;
386
- const error = new RemoteExecutionError("connect_failed", verdict.refusal);
387
- if (verdict.answered)
388
- this.connectRefusal = error;
389
- return error;
386
+ return verdict.answered ? this.seat.answer(connectRefusal("home_unconfirmed", verdict.refusal)).error : new RemoteExecutionError("connect_failed", verdict.refusal);
390
387
  }
391
388
  fillAt(sbx, root, origin) {
392
389
  return this.cfg.workspaceFill === undefined ? Promise.resolve({ ok: true }) : this.cfg.workspaceFill.fill(this.fillChannel(sbx), root, origin);
@@ -968,6 +965,19 @@ export class RemoteContainerExecutionEnv {
968
965
  async canonicalChain(p, abortSignal) {
969
966
  return runSymlinkChain(this.resolve(p), (cmd) => this.execInternal(cmd, { abortSignal }));
970
967
  }
968
+ async openBound(p, abortSignal) {
969
+ return openBoundOverShell(this.resolve(p), this.boundLane, abortSignal);
970
+ }
971
+ get boundLane() {
972
+ return {
973
+ run: async (cmd, _cap, abortSignal) => {
974
+ const r = await this.execInternal(cmd, { cwd: "/", timeout: this.cfg.dataTimeoutMs / 1000, ...(abortSignal !== undefined ? { abortSignal } : {}) });
975
+ return r.ok ? { ok: true, value: { stdout: Buffer.from(r.value.stdout, "utf8"), stderr: r.value.stderr, exitCode: r.value.exitCode } } : r;
976
+ },
977
+ payload: "base64",
978
+ boundSec: this.cfg.dataTimeoutMs / 1000,
979
+ };
980
+ }
971
981
  async canonicalPath(p, abortSignal) {
972
982
  const abs = this.resolve(p);
973
983
  const r = await this.execInternal(`realpath -- ${shellQuote(abs)}`, { abortSignal });
@@ -1,9 +1,13 @@
1
1
  import { FileError, RemoteExecutionError } from "@sema-agent/core";
2
+ import { isExecutionEnvRefusalCode } from "../execution-env-refusal.js";
3
+ function sayEnvNotOpened(code) {
4
+ return code === "connect_failed" || isExecutionEnvRefusalCode(code);
5
+ }
2
6
  function neverOpened(e) {
3
- if (e.code === "connect_failed")
7
+ if (sayEnvNotOpened(e.code))
4
8
  return true;
5
9
  const cause = e.cause;
6
- return cause instanceof RemoteExecutionError && cause.code === "connect_failed";
10
+ return cause instanceof RemoteExecutionError && sayEnvNotOpened(cause.code);
7
11
  }
8
12
  export function fileErrorFromExec(e, path) {
9
13
  const code = e.code === "aborted" ? "aborted" : neverOpened(e) ? "target_unavailable" : "unknown";
@@ -1,3 +1,4 @@
1
+ import { type PlatformPath } from "node:path";
1
2
  import { FileError, type InheritEnvPolicy, ExecutionError, RemoteExecutionError, SchedulerError, type SchedulerCapability, type ScheduledIntent, type SchedulerContext, type ScheduledTaskId, type ScheduledTaskSummary, type ExecutionEnv, type ExecResult, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type SymlinkChain, type BoundFileObject, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type VmLifecycleOptions, type ExecutionEnvFactory, type ExecutionEnvFactoryContext, BackgroundShellError, type BackgroundShellCapability, type BackgroundShellId, type BackgroundHardKillOutcome, type BackgroundPoll, type BackgroundSpawnOptions, type SecretEnvFinding } from "@sema-agent/core";
2
3
  import { type LaneHomeConfig } from "../lane-home-probe.js";
3
4
  import { type CustodyMember } from "./workspace-custody.js";
@@ -194,6 +195,11 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
194
195
  private tenure;
195
196
  /** Memoized lazy `mkdir -p` so concurrent first-use prepares the workspace exactly once. */
196
197
  private preparing?;
198
+ /**
199
+ * 建连座(`remote-env-connect-seat.ts`):`connect` 的唯一次序 —— 建工作目录在飞 ⇒ 等那一次(调用方的取消只退它自己的等待)· 已建好 ⇒ 当下的把手 · 否则建一次。
200
+ * 本车道不记拒:主张证不了的家目录在构造时就拒了(环境不铸),建目录的失败是那一次的(下一次重来)。
201
+ */
202
+ private readonly seat;
197
203
  private readonly handle;
198
204
  constructor(config?: HostEnvConfig);
199
205
  /** 收场的进度(托管表读):活着 ⇒ 缺席;{@link destroy} 开始 ⇒ 那一次收场。 */
@@ -214,6 +220,10 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
214
220
  ok: false;
215
221
  error: RemoteExecutionError;
216
222
  }>;
223
+ /** 工作目录已建好时 `connect` 的那一答。 */
224
+ private preparedAnswer;
225
+ /** 建工作目录失败时 `connect` 的那一答(那一次的失败:下一次重来)。 */
226
+ private prepareFailed;
217
227
  suspendVM(_options?: VmLifecycleOptions): Promise<{
218
228
  ok: true;
219
229
  value: SnapshotId;
@@ -474,6 +484,13 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
474
484
  private childHome;
475
485
  private fileError;
476
486
  }
487
+ /**
488
+ * host 环境配置座的拼法(构造期的 `workspaceDir` / `workspaceBase` / 托管 `sharedRoot`,与工厂托管判读的腿目录同一只):与执行缝同一只读者
489
+ * (`exec-path-law.ts` 的 `anchoredPath`)—— 绝对的原样,相对的锚在本进程工作目录上,不折 `..`、不解链接。win32 的盘相对拼写(`C:work`)相对的是
490
+ * **那只盘**上的工作目录:锚在本进程在那只盘上的工作目录(`resolve("C:")`)下、余下原样接上(不这样读,它会被当成一段名字接在工作目录后面:`<cwd>\C:work`)。
491
+ * `pathApi` 只为让 POSIX 上的钉量 win32 形;生产读本机的 `path`。
492
+ */
493
+ export declare function hostSeatPath(p: string, pathApi?: PlatformPath): string;
477
494
  /**
478
495
  * `ExecutionEnvFactory` for the TOC `host` backend (DUAL-MODE-DESIGN §5). Wiring this onto a deployment makes
479
496
  * its agent run DIRECTLY on the worker's machine (no container) — the local-coding-agent default. One fresh
@@ -1,11 +1,11 @@
1
1
  import { writeLogText } from "../observability/logger.js";
2
2
  import { spawn } from "node:child_process";
3
3
  import os from "node:os";
4
- import path from "node:path";
4
+ import path, {} from "node:path";
5
5
  import { anchoredPath, anchorUnder, joinedPath } from "./exec-path-law.js";
6
6
  import fs from "node:fs/promises";
7
7
  import { randomBytes } from "node:crypto";
8
- import { openSync, closeSync, readSync, statSync, truncateSync, unlinkSync, mkdirSync, rmdirSync, mkdtempSync, existsSync, realpathSync, accessSync, constants as FS_CONST } from "node:fs";
8
+ import { openSync, closeSync, readSync, statSync, truncateSync, unlinkSync, mkdirSync, rmdirSync, mkdtempSync, existsSync, accessSync, constants as FS_CONST } from "node:fs";
9
9
  import { StringDecoder } from "node:string_decoder";
10
10
  import { armPipeDestroyGrace, numEnvOr } from "./remote-shell.js";
11
11
  import { buildLivenessCut, commandBoundsOf } from "../exec-bounds.js";
@@ -13,7 +13,7 @@ import { buildCapturedOutput, createCutError } from "../exec-cut.js";
13
13
  import { hostBackgroundShellEnabled, hostExecSpoolEnabled } from "../config.js";
14
14
  import { IS_WIN32, resolveHostShell, hostShell, pinHostShell, spawnGroupOptions, killTreeHard, killTreeSoft, hardKillGroupNow, collapseWin32EnvKeys, adoptHostForegroundChild } from "./host-platform.js";
15
15
  import { BackgroundShellManager, seedMemStream, feedMemStream, drainMemStream } from "./background-shell-support.js";
16
- import { FileError, NodeExecutionEnv, inheritEnvPolicyProblem, ExecutionError, RemoteExecutionError, RollingTailBuffer, walkSymlinkChain, markTruncated, SchedulerError, BackgroundShellError, physicalPathOf as kernelRealPathOf, } from "@sema-agent/core";
16
+ import { FileError, NodeExecutionEnv, inheritEnvPolicyProblem, ExecutionError, RemoteExecutionError, RollingTailBuffer, walkSymlinkChain, markTruncated, SchedulerError, BackgroundShellError, physicalPathOf, } from "@sema-agent/core";
17
17
  import { capabilitiesFor } from "../execution-lane-caps.js";
18
18
  import { DEPLOYMENT_SHAPE_PREDICATES } from "../deployment-shape.js";
19
19
  import { LaneHomeSeat, laneHomeRefusal } from "../lane-home-probe.js";
@@ -21,7 +21,9 @@ import { composeHostSubprocessEnv, hostSubprocessEnv, withheldFromHostSubprocess
21
21
  import { nameTooLongCode } from "./remote-env-file-error.js";
22
22
  import { trackEphemeralHostWorkspace, untrackEphemeralHostWorkspace } from "./host-workspace-registry.js";
23
23
  import { createWorkspaceCustody } from "./workspace-custody.js";
24
+ import { canonicalPathOf, pathIsWithin } from "../memory-scope.js";
24
25
  import { OWNER_ONLY_FILE_MODE } from "./owner-only-file-mode.js";
26
+ import { createConnectSeat, withdrawOnAbort } from "./remote-env-connect-seat.js";
25
27
  const PROVIDER = "host";
26
28
  const DEFAULT_COMMAND_TIMEOUT_MS = 30 * 60_000;
27
29
  const inflightFsDrainWaitMs = () => numEnvOr("HOST_FS_DRAIN_WAIT_MS", 10_000, 1);
@@ -131,18 +133,23 @@ export class RemoteHostExecutionEnv {
131
133
  }
132
134
  tenure;
133
135
  preparing;
136
+ seat = createConnectSeat({
137
+ live: () => (this.prepared && !this.destroyed ? this.handle : undefined),
138
+ inFlight: () => (this.prepared ? undefined : this.preparing?.then(() => this.preparedAnswer(), (e) => this.prepareFailed(e))),
139
+ establish: (config) => withdrawOnAbort(this.ensurePrepared().then(() => this.preparedAnswer(), (e) => this.prepareFailed(e)), config?.abortSignal),
140
+ });
134
141
  handle;
135
142
  constructor(config = {}) {
136
143
  if (config.workspaceDir) {
137
- this.workspaceDir = path.resolve(config.workspaceDir);
144
+ this.workspaceDir = hostSeatPath(config.workspaceDir);
138
145
  const shared = config.custody?.sharedRoot;
139
- this.tenure = shared !== undefined ? { kind: "held", root: path.resolve(shared) } : BORROWED;
146
+ this.tenure = shared !== undefined ? { kind: "held", root: hostSeatPath(shared) } : BORROWED;
140
147
  }
141
148
  else {
142
- const base = config.workspaceBase ?? os.tmpdir();
149
+ const base = hostSeatPath(config.workspaceBase ?? os.tmpdir());
143
150
  const id = config.id ?? randomBytes(6).toString("hex");
144
151
  const suffix = randomBytes(4).toString("hex");
145
- this.workspaceDir = path.resolve(base, `sema-host-${sanitizeId(id)}-${suffix}`);
152
+ this.workspaceDir = anchorUnder(path, base, `sema-host-${sanitizeId(id)}-${suffix}`);
146
153
  this.tenure = { kind: "held", root: this.workspaceDir };
147
154
  }
148
155
  const inheritEnvProblem = inheritEnvPolicyProblem(config.inheritEnv);
@@ -185,18 +192,14 @@ export class RemoteHostExecutionEnv {
185
192
  return unsupported("connect(snapshotId)");
186
193
  if (this.destroyed)
187
194
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env already destroyed") };
188
- if (config?.abortSignal?.aborted)
189
- return { ok: false, error: new RemoteExecutionError("aborted", "connect aborted") };
190
- try {
191
- await this.ensurePrepared();
192
- }
193
- catch (e) {
194
- return { ok: false, error: new RemoteExecutionError("connect_failed", `failed to create host workspace ${this.workspaceDir}: ${errMsg(e)}`, asError(e)) };
195
- }
196
- if (config?.secrets?.length) {
197
- }
195
+ return this.seat.connect(config);
196
+ }
197
+ preparedAnswer() {
198
198
  return ok(this.handle);
199
199
  }
200
+ prepareFailed(e) {
201
+ return { ok: false, error: new RemoteExecutionError("connect_failed", `failed to create host workspace ${this.workspaceDir}: ${errMsg(e)}`, asError(e)) };
202
+ }
200
203
  async suspendVM(_options) {
201
204
  return unsupported("suspendVM");
202
205
  }
@@ -1327,7 +1330,7 @@ export class RemoteHostExecutionEnv {
1327
1330
  return false;
1328
1331
  const real = (x) => {
1329
1332
  try {
1330
- return kernelRealPathOf(x);
1333
+ return physicalPathOf(x);
1331
1334
  }
1332
1335
  catch {
1333
1336
  return undefined;
@@ -1408,6 +1411,13 @@ export class RemoteHostExecutionEnv {
1408
1411
  return new FileError("unknown", msg, p);
1409
1412
  }
1410
1413
  }
1414
+ export function hostSeatPath(p, pathApi = path) {
1415
+ if (pathApi.sep === "\\" && /^[A-Za-z]:(?![\\/])/.test(p)) {
1416
+ const driveCwd = pathApi.resolve(p.slice(0, 2));
1417
+ return p.length === 2 ? driveCwd : anchorUnder(pathApi, driveCwd, p.slice(2));
1418
+ }
1419
+ return anchoredPath(pathApi, process.cwd(), p);
1420
+ }
1411
1421
  function sanitizeId(id) {
1412
1422
  return id.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 64) || "task";
1413
1423
  }
@@ -1430,36 +1440,18 @@ function signalNumber(signal) {
1430
1440
  export function hostExecutionEnvFactory(config = {}, legOf = () => ({})) {
1431
1441
  const custody = createWorkspaceCustody();
1432
1442
  const heldElsewhere = (self, root) => custody.holder((m) => m !== self && m.heldWorkspace === root) !== undefined;
1443
+ const rootHolding = (dir) => custody.holder((m) => m.heldWorkspace !== undefined && pathIsWithin(m.heldWorkspace, dir, canonicalPathOf))?.heldWorkspace;
1433
1444
  return (ctx) => {
1434
1445
  const { minted, ...leg } = legOf(ctx);
1435
1446
  const identity = ctx.taskId ?? ctx.sessionId;
1436
1447
  return custody.envOf(identity, () => {
1437
1448
  const dir = leg.workspaceDir ?? config.workspaceDir;
1438
- const sharedRoot = dir === undefined ? undefined : custody.holder((m) => m.heldWorkspace !== undefined && dirWithin(dir, m.heldWorkspace))?.heldWorkspace;
1449
+ const sharedRoot = dir === undefined ? undefined : rootHolding(hostSeatPath(dir));
1439
1450
  const env = new RemoteHostExecutionEnv({ ...config, ...leg, id: config.id ?? identity, custody: { heldElsewhere, ...(sharedRoot !== undefined ? { sharedRoot } : {}) } });
1440
1451
  minted?.(env);
1441
1452
  return env;
1442
1453
  });
1443
1454
  };
1444
1455
  }
1445
- function dirWithin(dir, root) {
1446
- const inside = (d, r) => {
1447
- const rel = path.relative(r, d);
1448
- return rel === "" || (rel.split(path.sep)[0] !== ".." && !path.isAbsolute(rel));
1449
- };
1450
- return inside(path.resolve(dir), path.resolve(root)) || inside(physicalPathOf(dir), physicalPathOf(root));
1451
- }
1452
- function physicalPathOf(p) {
1453
- let head = path.resolve(p);
1454
- const tail = [];
1455
- while (!existsSync(head)) {
1456
- const up = path.dirname(head);
1457
- if (up === head)
1458
- return path.resolve(p);
1459
- tail.unshift(path.basename(head));
1460
- head = up;
1461
- }
1462
- return path.join(realpathSync(head), ...tail);
1463
- }
1464
1456
  export { localDockerExecutionEnvFactory, RemoteLocalDockerExecutionEnv } from "./remote-env-local-docker.js";
1465
1457
  //# sourceMappingURL=remote-env-host.js.map