@sema-agent/server 7.138.0 → 7.140.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (332) hide show
  1. package/MIGRATION.md +457 -7
  2. package/README.md +7 -4
  3. package/README.zh-CN.md +6 -4
  4. package/USAGE.md +91 -41
  5. package/deploy/sema-up/README.md +1 -1
  6. package/deploy/sema-up/lib/lanes.json +3 -2
  7. package/dist/adoption/runner.js +1 -1
  8. package/dist/adoption/sql.js +1 -1
  9. package/dist/adoption/wire.d.ts +3 -3
  10. package/dist/approval-card.d.ts +26 -26
  11. package/dist/bench/s1/arms.d.ts +1 -1
  12. package/dist/bench/s1/arms.js +1 -3
  13. package/dist/bench/s1/live-deps.js +3 -2
  14. package/dist/bench/s1/row.d.ts +5 -2
  15. package/dist/bench/s1/row.js +2 -22
  16. package/dist/bench/s1/run-firm.js +2 -2
  17. package/dist/boot/adoption.js +1 -1
  18. package/dist/boot/device-lane.d.ts +8 -3
  19. package/dist/boot/device-lane.js +7 -2
  20. package/dist/boot/execution-env.d.ts +4 -2
  21. package/dist/boot/execution-env.js +28 -34
  22. package/dist/boot/execution-lane.d.ts +9 -1
  23. package/dist/boot/execution-lane.js +6 -1
  24. package/dist/boot/governance-seams.d.ts +3 -2
  25. package/dist/boot/governance-seams.js +9 -11
  26. package/dist/boot/memory-boundary.d.ts +14 -51
  27. package/dist/boot/memory-boundary.js +10 -20
  28. package/dist/boot/memory-consolidation.d.ts +15 -11
  29. package/dist/boot/memory-consolidation.js +25 -14
  30. package/dist/boot/resolve-spec.d.ts +4 -6
  31. package/dist/boot/resolve-spec.js +10 -13
  32. package/dist/boot/retention-lane.d.ts +5 -5
  33. package/dist/boot/retention-lane.js +0 -6
  34. package/dist/boot/runner-deps.d.ts +6 -4
  35. package/dist/boot/runner-deps.js +3 -1
  36. package/dist/boot/session-faces.d.ts +0 -3
  37. package/dist/boot/session-faces.js +15 -8
  38. package/dist/boot/stage-02-web-admin-pull.js +2 -2
  39. package/dist/boot/stage-03-store-backend.d.ts +2 -6
  40. package/dist/boot/stage-03-store-backend.js +2 -3
  41. package/dist/boot/stage-04-stores.d.ts +2 -6
  42. package/dist/boot/stage-04-stores.js +5 -5
  43. package/dist/boot/stage-05-execution-env.d.ts +2 -6
  44. package/dist/boot/stage-05-execution-env.js +2 -2
  45. package/dist/boot/stage-06-runners.d.ts +2 -6
  46. package/dist/boot/stage-06-runners.js +2 -3
  47. package/dist/boot/stage-07-capability-layer.d.ts +2 -6
  48. package/dist/boot/stage-08-reapers.d.ts +2 -6
  49. package/dist/boot/stage-09-leader.d.ts +2 -6
  50. package/dist/boot/stage-09-leader.js +1 -2
  51. package/dist/boot/stage-10-http-server.d.ts +2 -6
  52. package/dist/boot/stage-10-http-server.js +7 -6
  53. package/dist/boot/stores.d.ts +3 -6
  54. package/dist/boot/stores.js +27 -40
  55. package/dist/boot/task-list-lane.d.ts +1 -1
  56. package/dist/boot/task-list-lane.js +1 -1
  57. package/dist/boot/web-admin.js +37 -11
  58. package/dist/capabilities/execution-lane.d.ts +2 -2
  59. package/dist/capabilities/mcp-probe.js +1 -1
  60. package/dist/capabilities/scenarios.d.ts +1 -1
  61. package/dist/capability-facts.d.ts +15 -7
  62. package/dist/capability-facts.js +8 -3
  63. package/dist/config-catalog.js +9 -8
  64. package/dist/config-invariants.d.ts +23 -3
  65. package/dist/config-invariants.js +42 -0
  66. package/dist/config-provider.d.ts +33 -37
  67. package/dist/config-provider.js +51 -30
  68. package/dist/config-types.d.ts +21 -7
  69. package/dist/config.d.ts +33 -0
  70. package/dist/config.js +46 -16
  71. package/dist/delegated-session-owner.d.ts +4 -3
  72. package/dist/delegated-session-owner.js +1 -1
  73. package/dist/deployment-shape.d.ts +10 -9
  74. package/dist/deployment-shape.js +4 -4
  75. package/dist/device-enrollment.d.ts +41 -2
  76. package/dist/device-enrollment.js +27 -12
  77. package/dist/device-store.d.ts +58 -85
  78. package/dist/device-store.js +19 -49
  79. package/dist/device-ws-hub.js +2 -1
  80. package/dist/env-name-allowlist-knobs.d.ts +5 -4
  81. package/dist/exec-cut.d.ts +60 -0
  82. package/dist/exec-cut.js +36 -0
  83. package/dist/execution-lane-caps.d.ts +115 -105
  84. package/dist/execution-lane-caps.js +76 -82
  85. package/dist/execution-placement.d.ts +259 -0
  86. package/dist/execution-placement.js +199 -0
  87. package/dist/field-conflict.d.ts +3 -2
  88. package/dist/field-conflict.js +1 -0
  89. package/dist/hosted-posture.d.ts +2 -2
  90. package/dist/http/admission.js +8 -19
  91. package/dist/http/checkpoint-ctx.d.ts +21 -12
  92. package/dist/http/checkpoint-ctx.js +4 -2
  93. package/dist/http/claimed-run.d.ts +1 -1
  94. package/dist/http/durable-run-leg.js +9 -5
  95. package/dist/http/execution-context.d.ts +218 -0
  96. package/dist/http/execution-context.js +330 -0
  97. package/dist/http/memory-compliance-failure-reply.d.ts +3 -1
  98. package/dist/http/memory-compliance-failure-reply.js +5 -0
  99. package/dist/http/memory-layer-reply.d.ts +30 -0
  100. package/dist/http/memory-layer-reply.js +24 -0
  101. package/dist/http/resume-legs.js +63 -36
  102. package/dist/http/retired-service-deps.d.ts +1 -1
  103. package/dist/http/retired-service-deps.js +1 -1
  104. package/dist/http/route-ctx.d.ts +14 -8
  105. package/dist/http/routes/adoption.js +1 -1
  106. package/dist/http/routes/background.d.ts +29 -1
  107. package/dist/http/routes/background.js +29 -2
  108. package/dist/http/routes/devices.d.ts +13 -19
  109. package/dist/http/routes/devices.js +21 -26
  110. package/dist/http/routes/memory-bundle.js +3 -0
  111. package/dist/http/routes/memory-compliance.js +8 -1
  112. package/dist/http/routes/memory-consolidation.js +12 -1
  113. package/dist/http/routes/memory-origin.js +14 -2
  114. package/dist/http/routes/memory-policy.js +5 -0
  115. package/dist/http/routes/session-sync.js +110 -15
  116. package/dist/http/routes/sessions.d.ts +6 -0
  117. package/dist/http/routes/sessions.js +143 -22
  118. package/dist/http/routes/side-query.d.ts +12 -4
  119. package/dist/http/routes/side-query.js +4 -5
  120. package/dist/http/routes/tasks.js +41 -33
  121. package/dist/http/server.d.ts +40 -44
  122. package/dist/http/server.js +13 -2
  123. package/dist/http/session-inherit-env.d.ts +3 -4
  124. package/dist/http/session-inherit-env.js +1 -1
  125. package/dist/http/wire-types.d.ts +8 -12
  126. package/dist/identity-source.d.ts +1 -1
  127. package/dist/identity-source.js +1 -1
  128. package/dist/index.d.ts +1 -0
  129. package/dist/lane-home-probe.d.ts +68 -42
  130. package/dist/lane-home-probe.js +53 -30
  131. package/dist/leader/wire.js +5 -4
  132. package/dist/memory-bundle-engine.d.ts +16 -9
  133. package/dist/memory-bundle-engine.js +29 -6
  134. package/dist/memory-operator-faces.d.ts +85 -30
  135. package/dist/memory-operator-faces.js +136 -30
  136. package/dist/memory-project-layer.d.ts +3 -21
  137. package/dist/memory-project-layer.js +11 -8
  138. package/dist/memory-scope-stores.d.ts +73 -0
  139. package/dist/memory-scope-stores.js +86 -0
  140. package/dist/memory-scope.d.ts +29 -5
  141. package/dist/memory-scope.js +16 -4
  142. package/dist/memory-sync-client.d.ts +70 -18
  143. package/dist/memory-sync-client.js +215 -117
  144. package/dist/memory-sync.d.ts +1 -1
  145. package/dist/memory-sync.js +3 -2
  146. package/dist/observability/cost-taxonomy.d.ts +2 -2
  147. package/dist/observability/fail-open.d.ts +12 -0
  148. package/dist/observability/fail-open.js +12 -0
  149. package/dist/observability/run-terminal.js +0 -1
  150. package/dist/orchestration/workflow-notify-journal.js +4 -4
  151. package/dist/org-memory-admission.d.ts +4 -3
  152. package/dist/org-memory-admission.js +6 -6
  153. package/dist/parked-decide.d.ts +34 -9
  154. package/dist/parked-decide.js +12 -19
  155. package/dist/parked-revive-surface.d.ts +12 -15
  156. package/dist/parked-revive-surface.js +3 -6
  157. package/dist/peer-seat.d.ts +13 -7
  158. package/dist/peer-seat.js +37 -3
  159. package/dist/plugins/adoption-log-sql.js +4 -4
  160. package/dist/plugins/approval-ask-store-file.d.ts +7 -0
  161. package/dist/plugins/approval-ask-store-file.js +11 -0
  162. package/dist/plugins/approval-ask-store-memory.d.ts +6 -0
  163. package/dist/plugins/approval-ask-store-memory.js +12 -0
  164. package/dist/plugins/approval-ask-store-sql.d.ts +22 -5
  165. package/dist/plugins/approval-ask-store-sql.js +44 -26
  166. package/dist/plugins/approval-nonce-store.js +4 -4
  167. package/dist/plugins/blob-backend.d.ts +4 -4
  168. package/dist/plugins/blob-backend.js +7 -7
  169. package/dist/plugins/breaker-state-sql.js +2 -2
  170. package/dist/plugins/caching-session-store.d.ts +6 -2
  171. package/dist/plugins/caching-session-store.js +9 -3
  172. package/dist/plugins/checkpoint-index-columns.js +2 -2
  173. package/dist/plugins/checkpoint-store-sql.d.ts +6 -5
  174. package/dist/plugins/checkpoint-store-sql.js +21 -23
  175. package/dist/plugins/device-store-sql.d.ts +25 -51
  176. package/dist/plugins/device-store-sql.js +68 -122
  177. package/dist/plugins/ensure-index.d.ts +1 -2
  178. package/dist/plugins/execution-placement-sql.d.ts +151 -0
  179. package/dist/plugins/execution-placement-sql.js +223 -0
  180. package/dist/plugins/file-history-store-sql.d.ts +9 -8
  181. package/dist/plugins/file-history-store-sql.js +9 -9
  182. package/dist/plugins/file-run-store.d.ts +6 -1
  183. package/dist/plugins/file-run-store.js +4 -0
  184. package/dist/plugins/fork-routing-session-store.d.ts +6 -0
  185. package/dist/plugins/fork-routing-session-store.js +16 -0
  186. package/dist/plugins/image-bake-store-sql.js +9 -9
  187. package/dist/plugins/image-index-sql.js +11 -11
  188. package/dist/plugins/leader-run-store-sql.js +4 -4
  189. package/dist/plugins/local-session-store.d.ts +149 -50
  190. package/dist/plugins/local-session-store.js +382 -107
  191. package/dist/plugins/mailbox-store-sql.d.ts +2 -1
  192. package/dist/plugins/mailbox-store-sql.js +4 -4
  193. package/dist/plugins/memory-engine-tidb.js +5 -5
  194. package/dist/plugins/memory-key-guards.d.ts +4 -0
  195. package/dist/plugins/memory-key-guards.js +1 -0
  196. package/dist/plugins/memory-optout-grant-store-sql.js +1 -1
  197. package/dist/plugins/memory-run-store.d.ts +1 -1
  198. package/dist/plugins/memory-sync-store-tidb.js +3 -3
  199. package/dist/plugins/one-time-migrations.d.ts +117 -8
  200. package/dist/plugins/one-time-migrations.js +408 -38
  201. package/dist/plugins/org-delivery-quota-store.js +2 -2
  202. package/dist/plugins/outcome-ledger-sql.d.ts +2 -2
  203. package/dist/plugins/outcome-ledger-sql.js +2 -2
  204. package/dist/plugins/permission-rule-store-sql.d.ts +17 -19
  205. package/dist/plugins/permission-rule-store-sql.js +29 -32
  206. package/dist/plugins/pg-pool.d.ts +7 -3
  207. package/dist/plugins/pg-session-storage.d.ts +31 -20
  208. package/dist/plugins/pg-session-storage.js +120 -52
  209. package/dist/plugins/remote-env-adb.d.ts +18 -19
  210. package/dist/plugins/remote-env-adb.js +91 -66
  211. package/dist/plugins/remote-env-device.d.ts +23 -31
  212. package/dist/plugins/remote-env-device.js +19 -17
  213. package/dist/plugins/remote-env-e2b.d.ts +27 -21
  214. package/dist/plugins/remote-env-e2b.js +91 -45
  215. package/dist/plugins/remote-env-file-error.d.ts +3 -1
  216. package/dist/plugins/remote-env-file-error.js +9 -3
  217. package/dist/plugins/remote-env-host.d.ts +31 -34
  218. package/dist/plugins/remote-env-host.js +91 -72
  219. package/dist/plugins/remote-env-k8s.d.ts +13 -29
  220. package/dist/plugins/remote-env-k8s.js +63 -55
  221. package/dist/plugins/remote-env-local-docker.d.ts +24 -12
  222. package/dist/plugins/remote-env-local-docker.js +49 -61
  223. package/dist/plugins/remote-env-ssh.d.ts +11 -19
  224. package/dist/plugins/remote-env-ssh.js +74 -57
  225. package/dist/plugins/remote-shell.d.ts +100 -3
  226. package/dist/plugins/remote-shell.js +166 -2
  227. package/dist/plugins/resume-anchor-store-sql.js +2 -2
  228. package/dist/plugins/retention-lane-store-sql.d.ts +1 -1
  229. package/dist/plugins/retention-lane-store-sql.js +2 -2
  230. package/dist/plugins/retention-store-sql.d.ts +10 -7
  231. package/dist/plugins/retention-store-sql.js +13 -7
  232. package/dist/plugins/run-store-sql.d.ts +13 -3
  233. package/dist/plugins/run-store-sql.js +20 -15
  234. package/dist/plugins/schema-consistency.d.ts +21 -11
  235. package/dist/plugins/schema-consistency.js +15 -9
  236. package/dist/plugins/schema-drift-sentence.d.ts +27 -0
  237. package/dist/plugins/schema-drift-sentence.js +38 -0
  238. package/dist/plugins/schema-ensure-legs.d.ts +2 -3
  239. package/dist/plugins/schema-ensure-legs.js +2 -2
  240. package/dist/plugins/schema-live-columns.d.ts +1 -1
  241. package/dist/plugins/session-capture-record-store-sql.js +1 -1
  242. package/dist/plugins/session-fork.d.ts +54 -0
  243. package/dist/plugins/session-fork.js +38 -1
  244. package/dist/plugins/session-row-sql.d.ts +22 -0
  245. package/dist/plugins/session-row-sql.js +81 -0
  246. package/dist/plugins/session-rules-column.d.ts +1 -1
  247. package/dist/plugins/session-rules-column.js +1 -1
  248. package/dist/plugins/session-store.d.ts +1 -1
  249. package/dist/plugins/session-workspace-cell.d.ts +1 -1
  250. package/dist/plugins/session-workspace-cell.js +1 -1
  251. package/dist/plugins/shared-memory-store-sql.js +2 -2
  252. package/dist/plugins/sql-driver.d.ts +10 -5
  253. package/dist/plugins/sql-driver.js +6 -6
  254. package/dist/plugins/sql-errors.js +1 -1
  255. package/dist/plugins/store-backend.d.ts +5 -7
  256. package/dist/plugins/store-backend.js +23 -11
  257. package/dist/plugins/store-contracts.d.ts +25 -5
  258. package/dist/plugins/task-list-store-sql.js +11 -11
  259. package/dist/plugins/tidb-pool.d.ts +12 -7
  260. package/dist/plugins/tidb-pool.js +13 -7
  261. package/dist/plugins/tidb-session-storage.js +4 -4
  262. package/dist/plugins/tidb-session-store.d.ts +57 -26
  263. package/dist/plugins/tidb-session-store.js +127 -59
  264. package/dist/plugins/tool-result-store-sql.d.ts +5 -4
  265. package/dist/plugins/tool-result-store-sql.js +21 -13
  266. package/dist/plugins/user-input-ledger-sql.js +2 -2
  267. package/dist/plugins/workflow-journal-store-sql.js +5 -5
  268. package/dist/plugins/workflow-run-store-sql.js +19 -19
  269. package/dist/plugins/write-behind-counter.d.ts +2 -3
  270. package/dist/plugins/write-behind-counter.js +5 -5
  271. package/dist/project-memory.d.ts +9 -15
  272. package/dist/project-memory.js +5 -4
  273. package/dist/request-key-closure.js +1 -1
  274. package/dist/request-value-closure.d.ts +6 -7
  275. package/dist/request-value-closure.js +1 -3
  276. package/dist/resource-suspend.d.ts +14 -12
  277. package/dist/resource-suspend.js +1 -1
  278. package/dist/resource-window.d.ts +4 -3
  279. package/dist/run-local.d.ts +6 -0
  280. package/dist/run-local.js +34 -35
  281. package/dist/runs.d.ts +10 -5
  282. package/dist/runs.js +52 -66
  283. package/dist/security.d.ts +38 -12
  284. package/dist/security.js +8 -2
  285. package/dist/server-secret-env.d.ts +9 -5
  286. package/dist/session-children-retire.d.ts +27 -9
  287. package/dist/session-children-retire.js +9 -5
  288. package/dist/session-incarnation.d.ts +20 -10
  289. package/dist/session-incarnation.js +5 -3
  290. package/dist/session-policy-wire.d.ts +1 -1
  291. package/dist/session-row.d.ts +114 -5
  292. package/dist/session-row.js +55 -5
  293. package/dist/session-titler.d.ts +2 -2
  294. package/dist/sql-engine-posture.d.ts +1 -1
  295. package/dist/sql-engine-posture.js +1 -1
  296. package/dist/{plugins → stores-sql}/background-agent-store-sql.d.ts +2 -2
  297. package/dist/{plugins → stores-sql}/background-agent-store-sql.js +8 -8
  298. package/dist/{plugins → stores-sql}/session-policy-store-sql.d.ts +7 -5
  299. package/dist/{plugins → stores-sql}/session-policy-store-sql.js +12 -12
  300. package/dist/{plugins → stores-sql}/usage-window-store-sql.d.ts +1 -1
  301. package/dist/{plugins → stores-sql}/usage-window-store-sql.js +3 -3
  302. package/dist/task-a2a.d.ts +1 -1
  303. package/dist/task-a2a.js +5 -6
  304. package/dist/task-cwd.d.ts +6 -1
  305. package/dist/task-mcp.d.ts +22 -9
  306. package/dist/task-mcp.js +17 -9
  307. package/dist/terminal.d.ts +27 -0
  308. package/dist/terminal.js +6 -0
  309. package/dist/tool-approval.d.ts +6 -0
  310. package/dist/tool-approval.js +8 -1
  311. package/dist/trace/project.d.ts +1 -0
  312. package/dist/trace/project.js +1 -0
  313. package/dist/trace/redact.d.ts +3 -1
  314. package/dist/trace/redact.js +80 -28
  315. package/dist/trace/self-minted-wire.d.ts +169 -2
  316. package/dist/trace/self-minted-wire.js +54 -1
  317. package/dist/url-refusal.d.ts +1 -1
  318. package/dist/web-admin-client/apply-effective.d.ts +17 -3
  319. package/dist/web-admin-client/apply-effective.js +20 -10
  320. package/dist/web-admin-client/candidate-gate.d.ts +11 -7
  321. package/dist/web-admin-client/candidate-gate.js +1 -1
  322. package/dist/web-admin-client/http-client.d.ts +5 -4
  323. package/dist/web-admin-client/http-client.js +2 -2
  324. package/dist/web-admin-client/read-warnings.d.ts +7 -17
  325. package/dist/web-admin-client/read-warnings.js +5 -9
  326. package/dist/web-admin-client/skills-mcp.js +3 -2
  327. package/dist/web-admin-client/types.d.ts +6 -0
  328. package/package.json +3 -3
  329. package/wire-changes/server@7.139.0.json +348 -0
  330. package/wire-changes/server@7.140.0.json +184 -0
  331. package/dist/http/device-placement.d.ts +0 -175
  332. package/dist/http/device-placement.js +0 -145
@@ -4,10 +4,12 @@ import fs from "node:fs/promises";
4
4
  import { spawn } from "node:child_process";
5
5
  import { hostSubprocessEnv } from "../server-secret-env.js";
6
6
  import { LANE_HOME_PROBE_COMMAND, LaneHomeSeat } from "../lane-home-probe.js";
7
- import { buildCwdEnvCommand, createCwdGuardedCall, shellQuote, armPipeDestroyGrace } from "./remote-shell.js";
7
+ import { appendFileStaged, rewriteAppend, buildCwdEnvCommand, createCwdGuardedCall, shellQuote, armPipeDestroyGrace, writeFileStaged } from "./remote-shell.js";
8
8
  import { signalNumber } from "./host-platform.js";
9
9
  import { buildLivenessCut, commandBoundsOf } from "../exec-bounds.js";
10
+ import { buildCapturedOutput, createCutError } from "../exec-cut.js";
10
11
  import { FileError, ExecutionError, RemoteExecutionError, withRetry, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
12
+ import { capabilitiesFor } from "../execution-lane-caps.js";
11
13
  import { classifyFsStderr, fileErrorFromExec } from "./remote-env-file-error.js";
12
14
  import { createPosixShellFs } from "./posix-shell-fs.js";
13
15
  const PROVIDER = "adb";
@@ -20,14 +22,14 @@ function lazyConnectExecError(e) {
20
22
  return e.code === "aborted" ? new ExecutionError("aborted", e.message, e) : new ExecutionError("shell_unavailable", `ADB lazy-connect failed: ${e.message}`, e);
21
23
  }
22
24
  function lazyConnectFileError(e, absPath) {
23
- return e.code === "aborted" ? new FileError("aborted", e.message, absPath, e) : new FileError("unknown", `ADB lazy-connect failed: ${e.message}`, absPath, e);
25
+ return fileErrorFromExec(e, absPath);
24
26
  }
25
27
  const isTcp = (serial) => /:\d+$/.test(serial);
26
28
  export function isAdbTransportLost(stderr) {
27
29
  return /device offline|device '.*' not found|no devices\/emulators found|connection reset|protocol fault|device disconnected/i.test(stderr);
28
30
  }
29
31
  export class RemoteAdbExecutionEnv {
30
- capabilities = { isolation: false, suspendable: false, canonicalPathAuthoritative: true, sharesProcessTable: false };
32
+ capabilities = capabilitiesFor({ remoteExec: { provider: "adb" } });
31
33
  cwd;
32
34
  connected = false;
33
35
  destroyed = false;
@@ -36,6 +38,7 @@ export class RemoteAdbExecutionEnv {
36
38
  return this.homeSeat.home;
37
39
  }
38
40
  homeSeat;
41
+ homeRefusal;
39
42
  cdPath = "unknown";
40
43
  withheldEnv = [];
41
44
  cfg;
@@ -51,7 +54,7 @@ export class RemoteAdbExecutionEnv {
51
54
  retry: { maxAttempts: config.retry?.maxAttempts ?? 3, backoffMs: config.retry?.backoffMs ?? 800 },
52
55
  };
53
56
  this.cwd = this.cfg.mountPath;
54
- this.homeSeat = new LaneHomeSeat({ lane: PROVIDER, declared: config.homeDir, verifiesDeclared: false, onAbsent: config.onHomeAbsent });
57
+ this.homeSeat = new LaneHomeSeat(PROVIDER, config);
55
58
  this.spawnFn = deps?.spawn ?? spawn;
56
59
  this.injectedConnectOnce = deps?.connectOnce;
57
60
  }
@@ -64,13 +67,22 @@ export class RemoteAdbExecutionEnv {
64
67
  async connect(config) {
65
68
  if (this.destroyed)
66
69
  return { ok: false, error: new RemoteExecutionError("connect_failed", "ADB env destroyed — create a fresh env instead of reviving") };
70
+ if (this.homeRefusal)
71
+ return { ok: false, error: this.homeRefusal };
67
72
  const policy = {
68
73
  retryableCodes: ["auth_transient", "connect_failed", "timeout"],
69
74
  maxAttempts: this.cfg.retry.maxAttempts,
70
75
  backoffMs: (failed) => this.cfg.retry.backoffMs * failed,
71
76
  };
72
77
  const once = this.injectedConnectOnce ?? ((s) => this.connectOnce(s));
73
- return withRetry((_attempt) => once(config?.abortSignal), policy, { signal: config?.abortSignal });
78
+ const refused = new AbortController();
79
+ const halt = config?.abortSignal ? AbortSignal.any([config.abortSignal, refused.signal]) : refused.signal;
80
+ return withRetry(async () => {
81
+ const r = await once(config?.abortSignal);
82
+ if (!r.ok && this.homeRefusal)
83
+ refused.abort();
84
+ return r;
85
+ }, policy, { signal: halt });
74
86
  }
75
87
  async ensureConnected() {
76
88
  if (this.destroyed)
@@ -126,7 +138,13 @@ export class RemoteAdbExecutionEnv {
126
138
  const home = await this.adb(this.deviceShellArgs(LANE_HOME_PROBE_COMMAND), { signal, timeoutMs: this.cfg.commandTimeoutMs });
127
139
  if (!home.ok && home.error.code === "aborted")
128
140
  return { ok: false, error: new RemoteExecutionError("aborted", "connect aborted while reading the device's home") };
129
- this.homeSeat.settle(home.ok && home.value.exitCode === 0 ? { ok: true, stdout: home.value.stdout } : { ok: false, reason: home.ok ? `exit ${home.value.exitCode}` : home.error.message });
141
+ const verdict = this.homeSeat.settle(home.ok && home.value.exitCode === 0 ? { ok: true, stdout: home.value.stdout } : { ok: false, reason: home.ok ? `exit ${home.value.exitCode}` : home.error.message });
142
+ if (verdict !== undefined) {
143
+ const error = new RemoteExecutionError("connect_failed", verdict.refusal);
144
+ if (verdict.answered)
145
+ this.homeRefusal = error;
146
+ return { ok: false, error };
147
+ }
130
148
  }
131
149
  this.connected = true;
132
150
  return ok(this.workspaceHandle());
@@ -341,57 +359,46 @@ export class RemoteAdbExecutionEnv {
341
359
  lines = lines.slice(0, options.maxLines);
342
360
  return ok(lines);
343
361
  }
362
+ stagedLane(content) {
363
+ return { upload: (dest, signal) => this.push(content, dest, signal), run: (cmd, signal) => this.fsExec(cmd, signal) };
364
+ }
344
365
  async writeFile(p, content, abortSignal) {
366
+ const gated = await this.fsGate(p);
367
+ if (gated)
368
+ return gated;
369
+ return writeFileStaged(this.stagedLane(content), this.resolve(p), abortSignal);
370
+ }
371
+ async appendFile(p, content, abortSignal) {
345
372
  const gated = await this.fsGate(p);
346
373
  if (gated)
347
374
  return gated;
348
375
  const abs = this.resolve(p);
349
- const dir = path.posix.dirname(abs);
350
- if (dir && dir !== "." && dir !== "/") {
351
- const mk = await this.fsExec(`mkdir -p ${shellQuote(dir)}`, abortSignal);
352
- if (!mk.ok)
353
- return { ok: false, error: fileErrorFromExec(mk.error, dir) };
354
- if (mk.value.exitCode !== 0)
355
- return { ok: false, error: new FileError("unknown", mk.value.stderr.trim() || `mkdir -p failed for ${dir}`, dir) };
356
- }
357
- let local;
376
+ return appendFileStaged(this.stagedLane(content), abs, abortSignal, (ctx) => rewriteAppend((signal) => this.readBinaryFile(abs, signal), (bytes, signal) => this.push(bytes, abs, signal), content, ctx));
377
+ }
378
+ async push(content, dest, abortSignal) {
379
+ let localDir;
358
380
  try {
359
- local = path.join(os.tmpdir(), `adb-push-${Date.now()}-${Math.random().toString(36).slice(2)}`);
381
+ localDir = await fs.mkdtemp(path.join(os.tmpdir(), "sema-adb-push-"));
382
+ const local = path.join(localDir, "data");
360
383
  await fs.writeFile(local, typeof content === "string" ? Buffer.from(content, "utf8") : Buffer.from(content));
361
- const r = await this.adb(["-s", this.cfg.serial, "push", local, abs], { signal: abortSignal, timeoutMs: this.cfg.dataTimeoutMs });
384
+ const r = await this.adb(["-s", this.cfg.serial, "push", local, dest], { signal: abortSignal, timeoutMs: this.cfg.dataTimeoutMs });
362
385
  if (!r.ok)
363
- return { ok: false, error: fileErrorFromExec(r.error, abs) };
386
+ return { ok: false, error: fileErrorFromExec(r.error, dest) };
364
387
  const out = `${r.value.stdout} ${r.value.stderr}`;
365
388
  if (/permission denied/i.test(out))
366
- return { ok: false, error: new FileError("permission_denied", out.trim(), abs) };
389
+ return { ok: false, error: new FileError("permission_denied", out.trim(), dest) };
367
390
  if (r.value.exitCode !== 0)
368
- return { ok: false, error: new FileError("unknown", r.value.stderr.trim() || `adb push failed for ${abs}`, abs) };
391
+ return { ok: false, error: new FileError("unknown", r.value.stderr.trim() || `adb push failed for ${dest}`, dest) };
369
392
  return ok(undefined);
370
393
  }
371
394
  catch (e) {
372
- return { ok: false, error: new FileError("unknown", e instanceof Error ? e.message : String(e), abs) };
395
+ return { ok: false, error: new FileError("unknown", e instanceof Error ? e.message : String(e), dest) };
373
396
  }
374
397
  finally {
375
- if (local)
376
- await fs.rm(local, { force: true }).catch(() => { });
398
+ if (localDir)
399
+ await fs.rm(localDir, { recursive: true, force: true }).catch(() => { });
377
400
  }
378
401
  }
379
- async appendFile(p, content, abortSignal) {
380
- const abs = this.resolve(p);
381
- const existing = await this.readBinaryFile(abs, abortSignal);
382
- let base;
383
- if (existing.ok)
384
- base = existing.value;
385
- else if (existing.error.code === "not_found")
386
- base = new Uint8Array(0);
387
- else
388
- return existing;
389
- const add = typeof content === "string" ? new TextEncoder().encode(content) : content;
390
- const merged = new Uint8Array(base.length + add.length);
391
- merged.set(base, 0);
392
- merged.set(add, base.length);
393
- return this.writeFile(abs, merged, abortSignal);
394
- }
395
402
  posixFsInst;
396
403
  get posixFs() {
397
404
  return (this.posixFsInst ??= createPosixShellFs({
@@ -451,23 +458,44 @@ export class RemoteAdbExecutionEnv {
451
458
  const outBufs = [];
452
459
  const outTail = binary ? undefined : new RollingTailBuffer();
453
460
  const errBuf = new RollingTailBuffer();
454
- const finish = (r) => {
461
+ const recordStderr = (bytes) => {
462
+ if (bytes.length > 0)
463
+ errBuf.push(bytes);
464
+ };
465
+ const emitStderr = (bytes) => {
466
+ if (bytes.length > 0)
467
+ opts?.onStderr?.(bytes.toString());
468
+ };
469
+ const takeStderr = (bytes) => {
470
+ recordStderr(bytes);
471
+ emitStderr(bytes);
472
+ };
473
+ const heldStderr = () => opts?.stderrFilter?.endStderr();
474
+ const captured = () => {
475
+ const held = heldStderr();
476
+ if (held)
477
+ recordStderr(held);
478
+ return buildCapturedOutput(outTail, errBuf);
479
+ };
480
+ const finish = (outcome) => {
455
481
  if (settled)
456
- return;
482
+ return false;
457
483
  settled = true;
484
+ const r = outcome();
458
485
  clearTimeout(timer);
459
486
  opts?.signal?.removeEventListener("abort", onAbort);
460
487
  resolve(r);
488
+ return true;
461
489
  };
462
490
  const timeoutMs = opts?.timeoutMs ?? this.cfg.commandTimeoutMs;
463
- const timer = setTimeout(() => {
464
- killGrace();
465
- finish({ ok: false, error: new ExecutionError("timeout", `adb ${args[0]} exceeded ${timeoutMs}ms`) });
466
- }, timeoutMs);
467
- const onAbort = () => {
468
- killGrace();
469
- finish({ ok: false, error: new ExecutionError("aborted", "adb command aborted") });
491
+ const cut = (c) => {
492
+ finish(() => {
493
+ killGrace();
494
+ return { ok: false, error: createCutError(c, captured()) };
495
+ });
470
496
  };
497
+ const timer = setTimeout(() => cut({ code: "timeout", message: `adb ${args[0]} exceeded ${timeoutMs}ms` }), timeoutMs);
498
+ const onAbort = () => cut({ code: "aborted", message: "adb command aborted" });
471
499
  if (opts?.signal) {
472
500
  if (opts.signal.aborted)
473
501
  return onAbort();
@@ -480,30 +508,27 @@ export class RemoteAdbExecutionEnv {
480
508
  outTail.push(d);
481
509
  opts?.onStdout?.(d.toString());
482
510
  });
483
- const takeStderr = (bytes) => {
484
- if (bytes.length === 0)
485
- return;
486
- errBuf.push(bytes);
487
- opts?.onStderr?.(bytes.toString());
488
- };
489
511
  child.stderr.on("data", (d) => takeStderr(opts?.stderrFilter ? opts.stderrFilter.filterStderr(d) : d));
490
- child.on("error", (e) => finish({ ok: false, error: new ExecutionError("spawn_error", `adb spawn failed (is '${this.cfg.adbPath}' installed?): ${e.message}`, e) }));
512
+ child.on("error", (e) => finish(() => ({ ok: false, error: new ExecutionError("spawn_error", `adb spawn failed (is '${this.cfg.adbPath}' installed?): ${e.message}`, e) })));
491
513
  child.on("close", (code, signal) => {
492
514
  if (forceSettleTimer)
493
515
  clearTimeout(forceSettleTimer);
494
- if (opts?.stderrFilter)
495
- takeStderr(opts.stderrFilter.endStderr());
496
- const err = errBuf.result();
497
- const stderr = markTruncated(err.text, err.droppedBytes);
498
516
  const exitCode = code ?? (signal ? 128 + (signalNumber(signal) ?? 9) : 0);
499
- if (binary) {
500
- const stdoutBytes = Buffer.concat(outBufs);
501
- finish(ok({ stdout: stdoutBytes.toString("utf8"), stderr, exitCode, stdoutBytes: new Uint8Array(stdoutBytes) }));
502
- }
503
- else {
517
+ const held = heldStderr();
518
+ const won = finish(() => {
519
+ if (held)
520
+ recordStderr(held);
521
+ const err = errBuf.result();
522
+ const stderr = markTruncated(err.text, err.droppedBytes);
523
+ if (binary) {
524
+ const stdoutBytes = Buffer.concat(outBufs);
525
+ return ok({ stdout: stdoutBytes.toString("utf8"), stderr, exitCode, stdoutBytes: new Uint8Array(stdoutBytes) });
526
+ }
504
527
  const out = outTail.result();
505
- finish(ok({ stdout: markTruncated(out.text, out.droppedBytes), stderr, exitCode }));
506
- }
528
+ return ok({ stdout: markTruncated(out.text, out.droppedBytes), stderr, exitCode });
529
+ });
530
+ if (won && held)
531
+ emitStderr(held);
507
532
  });
508
533
  });
509
534
  }
@@ -1,6 +1,7 @@
1
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";
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
+ import { type ExecutionPlacementSeat } from "../execution-placement.js";
4
5
  import type { DeviceWsHub } from "../device-ws-hub.js";
5
6
  import type { DeviceInstructionKind, DeviceOutcomeErrorCode } from "../device-ws-protocol.js";
6
7
  /** `WorkspaceHandle.provider` 与 `REMOTE_EXEC` 词表的那一个词,单点。 */
@@ -57,6 +58,8 @@ export interface DeviceEnvFactoryDeps {
57
58
  homeDir?: string;
58
59
  hub: DeviceWsHub;
59
60
  store: DeviceStore;
61
+ /** 会话店的执行席(绑定的唯一载体与唯一读;装配点交会话店的 `executionPlacement`,与准入链同一只)。工厂只调它的 `read(root)`。 */
62
+ seat: Pick<ExecutionPlacementSeat, "read">;
60
63
  enrollment: DeviceEnrollment;
61
64
  /** 指令级默认超时(ms;`DEVICE_EXEC_TIMEOUT_MS`,§4.7)。core 的 per-command `timeout` 可收紧它。 */
62
65
  execTimeoutMs?: number;
@@ -92,32 +95,8 @@ export declare class RemoteDeviceExecutionEnv implements RemoteExecutionEnv {
92
95
  /** core 7.56.0 [ref]:shell 子进程继承的环境里收不到的条目 —— 本车道 shell 的环境在设备上**构建**(本进程的 env 从不注入,只有引擎逐次交的 `env`
93
96
  * 这条显式授予),没有一条是从本进程继承的 ⇒ `[]`(core 契约:「`[]` when its shell environment is built rather than inherited」)。 */
94
97
  readonly withheldEnv: readonly [];
95
- /**
96
- * 🔴 `suspendable: false` 在 core 契约里**同时断言**「工作区在目标上外部持久」(remote-env.ts 的
97
- * load-bearing 不变量)—— 设备工作区 = 员工机磁盘,真持久 ✅,park-only 降级腿因此合法可用。
98
- * 谁要把 executor 的工作区改成临时目录,先来改这一行:那一刻这个 `false` 就变成谎话,park 赎回会
99
- * 落到一个空工作区上而链路上没有任何一层会报错。
100
- */
101
- /**
102
- * 🔴 **[ref] / core 7.20.0([ref])`canonicalPathAuthoritative: true`** —— 本适配器的 `canonicalPath`
103
- * 就是**命令真正跑的那块文件系统**的权威答案(设备 WS 协议的**必答** `canonicalPath` op)。声明它 = 告诉引擎:整链答不出来是**解析器出了事**
104
- * (RPC 断、传输掉),不是「一个它看不见的命名空间」⇒ 读边界的执行期复核**拒**,而不是让词法判决站着。
105
- * 缺席是今天每个适配器的读法(整链沉默 ⇒ 词法判决成立、命令照跑),所以这一声明是**收紧**方向。
106
- * 严格读法 `=== true`(core `canonicalPathIsAuthoritative`);`isSuspendable` 一族同样严格,对本仓的
107
- * 布尔字面量零影响(四键都写死字面 `true`/`false`,不存在「非布尔值被读成缺席」那一形 —— 那一形由
108
- * core 的 `config.execution_env_capability_invalid` 普查响亮播报,本仓的受众表已登记)。
109
- * 谁该声明由闭集门看着(`test/execution-lane-caps.test.ts` 的逐 lane 对账:新 lane 不表态 = 编译红)。
110
- */
111
- /** `sharesProcessTable`(core 7.57.0,必填):命令跑在注册设备上(设备执行器),不在本进程所在的进程表。 */
112
- readonly capabilities: {
113
- isolation: boolean;
114
- suspendable: boolean;
115
- canonicalPathAuthoritative: boolean;
116
- sharesProcessTable: boolean;
117
- };
118
- readonly externalContentTarget = true;
119
- /** [ref]:设备路径不在 worker 宿主文件系统上 —— host-path 回执消费方据此拒绝把它当宿主路径呈现。 */
120
- readonly hostLocalPaths = false;
98
+ /** 能力块的唯一作者 = `capabilitiesFor`(`execution-lane-caps.ts`;逐键表态与理由在那里,本类零能力字面量)。 */
99
+ readonly capabilities: import("@sema-agent/core").ExecutionEnvCapabilities;
121
100
  cwd: string;
122
101
  /** 可选面(presence-typed,见文件头「铁律」段):构造期按设备声明装配,缺席就是缺席。 */
123
102
  readLink?: (path: string, abortSignal?: AbortSignal) => Promise<Result<string, FileError>>;
@@ -332,11 +311,24 @@ export declare class RemoteDeviceExecutionEnv implements RemoteExecutionEnv {
332
311
  * `DEVICE_ERROR_CODES` 闭集(装配 / 数据错误取 {@link DEVICE_FACTORY_CONFIG_CODES}:placement 根为空 · 有效工作目录非绝对路径),句子里的 `[码]` 与 `.code` 同值。修前抛裸 `Error`,码只在句子里
333
312
  * (有效工作目录那一只直到 7.135.0 仍是裸 `Error`;钉:本工厂与 {@link effectiveDeviceCwd} 的函数体里没有 `new Error(`)。
334
313
  *
335
- * 🔴 **续跑腿的投递围栏**(core 7.54.0 [ref] r3 / [ref],`ExecutionEnvFactoryContext.resumeWorkspace`):行记下了设备(`resumeWorkspace.deviceId`)⇒
336
- * 本工厂**只**在那台设备上铸 —— placement 根此刻绑的不是它(换绑过)⇒ 抛 typed `device.identity_mismatch`(换绑后赎回的同一个词,零新码),
337
- * 句子点名两台设备;不连另一台、不改道(新设备没有那个工作区)。读当前绑定只为判出这一拒与取属主,铸的设备恒是行记下的那一台。
338
- * 「绑的就是它但现在不在线」与新跑同形:构造成功、`connect()` 回 typed `device.offline`(从不连别的设备)。
339
- * 行没记设备(非设备车道 / 早于该字段的行)⇒ 不声明要兑现的设备,按普通路由走。句柄(`workspaceHandle()`)恒报连接真到达的设备,不抄 `resumeWorkspace`。
314
+ * 🔴 **工厂认设备不认住客**(读口一只 = 会话店执行席 `seat.read(root)`,与解析器 / 换绑门 / 执行放置读面 / 准入链同一只读):按 placement 根读**当前**格,
315
+ * 与这条腿带来的设备句柄 `honor` 比,不同即 typed 拒。判序(一次行读):
316
+ * root = ctx.placementRootSessionId // 空 ⇒ config.placement_root_invalid
317
+ * honor = ctx.resumeWorkspace?.deviceId
318
+ * cell = seat.read(root).cell // 行不在 / 格缺 / mirror / writer(device, ∅) ⇒ device.not_bound(唯一读法 `boundDeviceOf`)
319
+ * honor ≠ undefined ∧ 格上设备 ≠ honor ⇒ device.identity_mismatch(句点名两台,零连接)
320
+ * readDeviceForAdmission(格上设备, 格上属主)
321
+ * 工厂**不**读 `task_run`、不读子代分区行、不按 `ctx.taskId`、不比化身、不比属主(住客谓词只用于写与 judged 复核,不是工厂的读者)。
322
+ * 续跑腿(core 7.54.0 [ref] r3 / [ref],`ExecutionEnvFactoryContext.resumeWorkspace`):行记下了设备 ⇒ 本工厂**只**在那台设备上铸 —— 根此刻绑的不是它(换绑过)
323
+ * ⇒ `device.identity_mismatch`(换绑后赎回的同一个词,零新码);不连另一台、不改道(新设备没有那个工作区)。「绑的就是它但现在不在线」与新跑同形:
324
+ * 构造成功、`connect()` 回 typed `device.offline`(从不连别的设备)。行没记设备(早于该字段的行)⇒ 引擎已命名的「行未记设备 ⇒ 普通路由」臂,按根读当前格。
325
+ * 句柄(`workspaceHandle()`)恒报连接真到达的设备,不抄 `resumeWorkspace`。
326
+ *
327
+ * 🔴 **过渡期(条件句)**:新派后代腿的放置句柄(引擎在 `ExecutionEnvFactoryContext` 上的放置工作区字段,`honor` 的第二来源)**只在本仓所基的引擎版本带它时**
328
+ * 进判序;本仓今天所基的引擎上下文没有这个字段 ⇒ `honor` 只取续跑腿的 `resumeWorkspace`,新派后代腿按根读当前格。本仓**不**注入任何替代见证、不造任何行
329
+ * (修在源头)。敞口(在那个字段到货之前成立):活过父 turn 的后台子代,在根换绑之后新派的腿会落到根当前设备 —— 换绑半边由换绑门的子代活性判关闭(归换绑门那一车),
330
+ * 删除半边另有票。字段到货后的形:`honor = ctx.resumeWorkspace?.deviceId ?? <放置句柄>.deviceId`,放置句柄的 provider 不是本车道 ⇒ 装配拒(`config.placement_root_invalid` 同级)。
331
+ * 本仓任何文本不得宣称工厂「一律按住客定址」。
340
332
  */
341
333
  export declare function deviceExecutionEnvFactory(deps: DeviceEnvFactoryDeps): ExecutionEnvFactory;
342
334
  /**
@@ -1,6 +1,9 @@
1
1
  import { posix } from "node:path";
2
2
  import { ExecutionError, FileError, RemoteExecutionError, } from "@sema-agent/core";
3
+ import { capabilitiesFor } from "../execution-lane-caps.js";
3
4
  import { DEVICE_ERROR_CODES, isDeviceAbsolutePath, } from "../device-store.js";
5
+ import { boundDeviceOf } from "../execution-placement.js";
6
+ import { createCutError, createExecFailure } from "../exec-cut.js";
4
7
  export const DEVICE_PROVIDER = "device";
5
8
  const ok = (value) => ({ ok: true, value });
6
9
  const STREAM_PENDING_MAX_BYTES = 2 * 1024 * 1024;
@@ -126,9 +129,7 @@ export class RemoteDeviceExecutionEnv {
126
129
  homeDir;
127
130
  cdPath = "unknown";
128
131
  withheldEnv = [];
129
- capabilities = { isolation: false, suspendable: false, canonicalPathAuthoritative: true, sharesProcessTable: false };
130
- externalContentTarget = true;
131
- hostLocalPaths = false;
132
+ capabilities = capabilitiesFor({ remoteExec: { provider: "device" } });
132
133
  cwd;
133
134
  readLink;
134
135
  writeFileExclusive;
@@ -227,7 +228,8 @@ export class RemoteDeviceExecutionEnv {
227
228
  }
228
229
  let admitted;
229
230
  try {
230
- admitted = await this.cfg.enrollment.admitSession({ rootSessionId: this.cfg.rootSessionId, requester: this.cfg.owner });
231
+ const placement = await this.cfg.enrollment.readPlacement(this.cfg.rootSessionId);
232
+ admitted = await this.cfg.enrollment.admitSession({ rootSessionId: this.cfg.rootSessionId, placement, requester: this.cfg.owner });
231
233
  }
232
234
  catch (err) {
233
235
  return {
@@ -302,7 +304,7 @@ export class RemoteDeviceExecutionEnv {
302
304
  return { ok: false, error: new ExecutionError("shell_unavailable", "execution env already destroyed") };
303
305
  const failure = await this.ensureConnected();
304
306
  if (failure)
305
- return { ok: false, error: new ExecutionError(failure.exec, failure.message) };
307
+ return { ok: false, error: createExecFailure(failure.exec, failure.message) };
306
308
  return undefined;
307
309
  }
308
310
  async fsGate(p) {
@@ -381,15 +383,14 @@ export class RemoteDeviceExecutionEnv {
381
383
  case "env_terminal":
382
384
  return { ok: false, error: new ExecutionError("shell_unavailable", ENV_TERMINAL_MESSAGE) };
383
385
  case "rejected":
384
- return { ok: false, error: new ExecutionError(DEVICE_REJECT_TO_EXEC_ERROR[res.reject.code], rejectText(res.reject)) };
386
+ return { ok: false, error: createExecFailure(DEVICE_REJECT_TO_EXEC_ERROR[res.reject.code], rejectText(res.reject)) };
385
387
  case "never_started":
386
388
  case "settled": {
387
389
  const o = res.outcome;
388
- if (!o.ok) {
389
- return { ok: false, error: new ExecutionError(DEVICE_OUTCOME_TO_EXEC_ERROR[o.errorCode], o.message, undefined, { stdout, stderr }) };
390
- }
390
+ if (!o.ok)
391
+ return { ok: false, error: createExecFailure(DEVICE_OUTCOME_TO_EXEC_ERROR[o.errorCode], o.message, res.kind === "settled" ? { stdout, stderr } : undefined) };
391
392
  if (o.kind === "cancelled") {
392
- return { ok: false, error: new ExecutionError("aborted", `the device reported the command as ${o.state} after a cancel`, undefined, { stdout, stderr }) };
393
+ return { ok: false, error: createCutError({ code: "aborted", message: `the device reported the command as ${o.state} after a cancel` }, { stdout, stderr }) };
393
394
  }
394
395
  if (o.kind !== "exec")
395
396
  return { ok: false, error: new ExecutionError("unknown", `device returned a "${o.kind}" outcome for an exec — protocol violation`) };
@@ -799,17 +800,18 @@ export function deviceExecutionEnvFactory(deps) {
799
800
  throw deviceFactoryRefusal(DEVICE_FACTORY_CONFIG_CODES.PLACEMENT_ROOT_INVALID, `device lane: ExecutionEnvFactoryContext.placementRootSessionId is empty [${DEVICE_FACTORY_CONFIG_CODES.PLACEMENT_ROOT_INVALID}] — ` +
800
801
  "the device placement is keyed on the run tree's placement root; defaulting to the child sessionId would read as a NEW placement (v2 §4.3.2 / core design/380 O1①)");
801
802
  }
802
- const binding = await deps.store.getSessionBinding(rootSessionId);
803
+ const placement = await deps.seat.read(rootSessionId);
804
+ const binding = placement === undefined ? undefined : boundDeviceOf(placement.cell);
803
805
  if (!binding) {
804
806
  throw deviceFactoryRefusal(DEVICE_ERROR_CODES.NOT_BOUND, `device lane: no device binding for placement root ${rootSessionId} [${DEVICE_ERROR_CODES.NOT_BOUND}] — ` +
805
807
  "the submit-time admission chain must bind the root session to a device before a run reaches the factory (v2 §4.3.2)");
806
808
  }
807
- const recordedDevice = ctx.resumeWorkspace?.deviceId;
808
- if (recordedDevice !== undefined && recordedDevice !== binding.deviceId) {
809
- throw deviceFactoryRefusal(DEVICE_ERROR_CODES.IDENTITY_MISMATCH, `device lane: this resume rebuilds the workspace on device ${recordedDevice}, but placement root ${rootSessionId} is now bound to ${binding.deviceId} [${DEVICE_ERROR_CODES.IDENTITY_MISMATCH}] — ` +
809
+ const honor = ctx.resumeWorkspace?.deviceId;
810
+ if (honor !== undefined && honor !== binding.id) {
811
+ throw deviceFactoryRefusal(DEVICE_ERROR_CODES.IDENTITY_MISMATCH, `device lane: this resume rebuilds the workspace on device ${honor}, but placement root ${rootSessionId} is now bound to ${binding.id} [${DEVICE_ERROR_CODES.IDENTITY_MISMATCH}] — ` +
810
812
  "a parked call does not follow a rebind (the new device has no copy of that workspace); finish or cancel the parked task, then start it again on the device now bound");
811
813
  }
812
- const view = await deps.store.readDeviceForAdmission(binding.deviceId, binding.owner);
814
+ const view = await deps.store.readDeviceForAdmission(binding.id, binding.owner);
813
815
  if (!view) {
814
816
  throw deviceFactoryRefusal(DEVICE_ERROR_CODES.NOT_FOUND, `device lane: the bound device is gone or not owned by the binding's principal [${DEVICE_ERROR_CODES.NOT_FOUND}]`);
815
817
  }
@@ -821,13 +823,13 @@ export function deviceExecutionEnvFactory(deps) {
821
823
  hub: deps.hub,
822
824
  enrollment: deps.enrollment,
823
825
  rootSessionId,
824
- deviceId: binding.deviceId,
826
+ deviceId: binding.id,
825
827
  owner: binding.owner,
826
828
  cwd: effectiveDeviceCwd(deps.perSessionCwd?.get(ctx.sessionId), ctx.parentCwd, view.row),
827
829
  ...(deps.homeDir === undefined ? {} : { homeDir: deps.homeDir }),
828
830
  ...(deps.perSessionShellEnv?.get(ctx.sessionId) === undefined ? {} : { shellEnv: deps.perSessionShellEnv.get(ctx.sessionId) }),
829
831
  execTimeoutMs,
830
- supports: deps.hub.declaredSupports(binding.deviceId),
832
+ supports: deps.hub.declaredSupports(binding.id),
831
833
  ...(deps.logger === undefined ? {} : { logger: deps.logger }),
832
834
  });
833
835
  };
@@ -21,6 +21,7 @@
21
21
  */
22
22
  import { Sandbox, type SandboxOpts, type SandboxConnectOpts } from "e2b";
23
23
  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 ExecutionEnvFactory, type VmLifecycleOptions, type BackgroundShellCapability, type BackgroundShellId, type BackgroundShellError, type BackgroundSpawnOptions, type BackgroundPoll } from "@sema-agent/core";
24
+ import { type LaneHomeConfig } from "../lane-home-probe.js";
24
25
  /** Options accepted by `ExecutionEnv.exec` (core does not re-export the type; derive it from the seam). */
25
26
  type ExecOpts = Parameters<ExecutionEnv["exec"]>[1];
26
27
  /** Minimal optional observability hooks (kept structural so the adapter stays standalone-testable). */
@@ -34,22 +35,13 @@ export interface RemoteEnvObservers {
34
35
  };
35
36
  }
36
37
  /** Construction config for {@link RemoteContainerExecutionEnv}. Secrets/resolved values are control-plane supplied. */
37
- export interface RemoteContainerEnvConfig extends RemoteEnvObservers {
38
+ export interface RemoteContainerEnvConfig extends RemoteEnvObservers, LaneHomeConfig {
38
39
  /** E2B API key. Resolved by the trusted control plane from env — never baked into an image or committed. */
39
40
  apiKey: string;
40
41
  /** E2B template id (image). Defaults to the E2B base template. */
41
42
  template?: string;
42
43
  /** Sandbox lifetime in ms (E2B billing window). Default {@link DEFAULT_TIMEOUT_MS}. */
43
44
  timeoutMs?: number;
44
- /**
45
- * [ref]④ / core [ref] —— 本执行环境的 **home 目录**(`ExecutionEnv.homeDir` 座,绝对路径,本环境自己的
46
- * 命名空间里)。`~/…` 形权限规则(`Edit(~/.ssh/**)`)对**跑在本 env 里的调用**就以它为基。
47
- * 缺席 ⇒ 本 env 声明「我没有 home」,core 对 `~/` 规则判 `unreadable`(fail-closed 的 ask),
48
- * 绝不回落引擎进程自己的 home(那在远端腿上是另一个用户的另一个目录 —— 静默守错目录正是 [ref] 的病)。
49
- * 值由 `boot/execution-env.ts` 从**唯一属主** `execution-lane-caps.ts` 的 `executionLaneHomeDir` 递进来;
50
- * adapter 不自算(两处各算一份 = 422 门与引擎对同一条规则给相反答案)。
51
- */
52
- homeDir?: string;
53
45
  /** Workspace root inside the VM; remote paths normalize against it. Default {@link DEFAULT_MOUNT_PATH}. */
54
46
  mountPath?: string;
55
47
  /** Outbound network for the sandbox (clay: service needs external-network capability). Default `true`. */
@@ -99,8 +91,9 @@ export interface RemoteContainerEnvDeps {
99
91
  export declare class RemoteContainerExecutionEnv implements RemoteExecutionEnv, BackgroundShellCapability {
100
92
  /** Current working directory for relative paths (FileSystem contract; mutable, core may reassign). */
101
93
  cwd: string;
102
- /** core `ExecutionEnv.homeDir`([ref]):本沙箱/目标机上执行用户的 home。见配置同名字段。 */
103
- readonly homeDir?: string | undefined;
94
+ /** core `ExecutionEnv.homeDir`([ref]):沙箱里执行用户的 home —— 主张在场 ⇒ 主张(每只沙箱建起来 / 恢复时与沙箱里的 `$HOME` 对证过),
95
+ * 否则建沙箱那一拍实测的 `$HOME`,见 {@link LaneHomeSeat}。读时才取(没主张时沙箱建起来之前缺席)。 */
96
+ get homeDir(): string | undefined;
104
97
  /** core 7.49.0 [ref]:shell 子进程真拿到的 `CDPATH` 条目 —— 本车道的命令跑在 e2b 沙箱里(模板镜像的 env),本类不读那份 env ⇒ `"unknown"`(core 契约:说不出就说 unknown,
105
98
  * 不许报一张没读过的空表;删除守卫因此对裸 `cd <相对名>` 之后的删除问人,而不是按「没有 CDPATH」放行)。 */
106
99
  readonly cdPath: "unknown";
@@ -123,6 +116,10 @@ export declare class RemoteContainerExecutionEnv implements RemoteExecutionEnv,
123
116
  * and the post-await re-check in BOTH async-provision paths ({@link doConnect} and {@link doResumeVM}) against
124
117
  * a destroy that races an in-flight provision/restore. */
125
118
  private destroyed;
119
+ /** 家目录的座(主张在场 ⇒ 每只沙箱对证 · 否则建沙箱那一拍实测 · 实测缺席 ⇒ 缺席 + 一行 warn)。 */
120
+ private readonly homeSeat;
121
+ /** 家目录:沙箱答了而与主张不一致(或答的不是绝对路径)⇒ 这只环境的拒,记一次、此后每次连接 / 恢复原样回它(同一模板同一个答案:不再建沙箱、不再问)。 */
122
+ private homeRefusal?;
126
123
  /** In-flight connect/resume, memoized so concurrent lazy first-use OR resume provisions exactly one VM (no double-bill). */
127
124
  private connecting?;
128
125
  /** Count of exec/execStream calls in flight — `suspendVM` refuses while > 0 (council #4: never snapshot a running command). */
@@ -133,15 +130,8 @@ export declare class RemoteContainerExecutionEnv implements RemoteExecutionEnv,
133
130
  * SAME env instance skips the reconnect and every later fs/exec runs against the dead VM. */
134
131
  private paused;
135
132
  constructor(config: RemoteContainerEnvConfig, deps?: RemoteContainerEnvDeps);
136
- /** [ref] §9: E2B is a FULL RemoteExecutionEnv — an isolated, snapshot-suspendable microVM. (SSH/ADB
137
- * adapters declare {isolation:false, suspendable:false} — real machines/devices, not snapshotable.) */
138
- /** `sharesProcessTable`(core 7.57.0,必填):命令跑在云沙箱里,不在本进程所在的进程表。 */
139
- readonly capabilities: {
140
- isolation: boolean;
141
- suspendable: boolean;
142
- canonicalPathAuthoritative: boolean;
143
- sharesProcessTable: boolean;
144
- };
133
+ /** 能力块的唯一作者 = `capabilitiesFor`(`execution-lane-caps.ts`;逐键表态与理由在那里,本类零能力字面量)。 */
134
+ readonly capabilities: import("@sema-agent/core").ExecutionEnvCapabilities;
145
135
  workspaceHandle(): WorkspaceHandle;
146
136
  /**
147
137
  * Public host for a port inside the sandbox (E2B reverse proxy → `<port>-<id>.e2b.app`). The LSP sidecar
@@ -212,6 +202,13 @@ export declare class RemoteContainerExecutionEnv implements RemoteExecutionEnv,
212
202
  ok: false;
213
203
  error: RemoteExecutionError;
214
204
  }>;
205
+ /**
206
+ * 家目录:座要问({@link LaneHomeSeat.asks}:主张在场 ⇒ 对证;没主张 ⇒ 量到绝对路径为止)⇒ 在这只沙箱里问一次 `$HOME`(前台 `commands.run`,与命令同一个
207
+ * 模板缺省用户、同一份沙箱 env),读数交座。返回这只沙箱的拒(`connect_failed`,拒句 = 同一只判官的句子),或 `undefined`(照用)。
208
+ * 沙箱答了的拒记在环境上(同一模板同一个答案);没答上(非零退出 / RPC 失败 / 输出里没有标记行)⇒ 这一次不开、不记,下一次重问;调用方中止 ⇒ `aborted`、不交座。
209
+ * 上界 = 控制面 RPC 的墙钟(`rpcTimeoutMs`:问一个环境变量,不是跑用户命令)。
210
+ */
211
+ private homeVerdict;
215
212
  /** BL-31 — (re)start the periodic sandbox-lifetime extension. `setTimeout(timeoutMs)` resets the auto-kill window
216
213
  * from now; we re-arm at ~80% of the (clamped, ≤1h) window so a long-held sandbox never expires mid-run. A no-op
217
214
  * when paused/destroyed; `unref`'d so it never keeps the process alive; cleared on destroy. */
@@ -254,7 +251,14 @@ export declare class RemoteContainerExecutionEnv implements RemoteExecutionEnv,
254
251
  abortSignal?: AbortSignal;
255
252
  }): Promise<Result<string[], FileError>>;
256
253
  readBinaryFile(p: string, abortSignal?: AbortSignal): Promise<Result<Uint8Array, FileError>>;
254
+ /** 暂存写的两只面(规则与提交脚本的唯一属主 = `remote-shell.ts` `writeFileStaged` / `appendFileStaged`;本腿是原生 SDK 腿,与四条
255
+ * shell 腿共用的是这只编排,不是 `posix-shell-fs` 基座;父目录与私有暂存目录由编排的准备那一步建):上传 = `files.write` 整份写到私有目录里的
256
+ * 暂存件;提交 = 一次 `exec`。两者同一身份(`files.write` 与 `commands.run` 都不带 `user`,沙箱侧取同一个缺省用户)。 */
257
+ private stagedLane;
258
+ /** 整旧或整新(暂存 → 一次 `mv -f`)。修前 `files.write` 直落目标:上传中途断 / 取消时目标剩什么取决于沙箱侧的落地实现,本适配器不依赖它。 */
257
259
  writeFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
260
+ /** 追加(暂存要加的字节 → 一次 `cat >>`;inode 不换、已有字节不重写)。修前 = 读旧 + 拼 + 整份 `files.write`(O(文件);中途断 ⇒
261
+ * 半份)。目录不收新名字 / 私有目录收不成私有时的退路 = 旧形(`rewriteAppend`:读旧 + 拼 + 整份 `files.write` 到目标;只有写那一步经 `ctx.inPlace`)。 */
258
262
  appendFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
259
263
  fileInfo(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo, FileError>>;
260
264
  listDir(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo[], FileError>>;
@@ -340,6 +344,8 @@ export declare class RemoteContainerExecutionEnv implements RemoteExecutionEnv,
340
344
  private tempResult;
341
345
  private toFileInfo;
342
346
  private fileError;
347
+ /** A thrown provider error → this exec's error. The two cuts (aborted, the provider's own command deadline) are a command that RAN: they carry what it had
348
+ * written (`captured`, `createCutError`); an unclassified fault does not (its outcome is unknown, the engine reads no partial output from it). */
343
349
  private execError;
344
350
  private remoteError;
345
351
  }