@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
@@ -7,10 +7,13 @@ import { openSync, closeSync, readSync, statSync, truncateSync, unlinkSync, mkdi
7
7
  import { StringDecoder } from "node:string_decoder";
8
8
  import { armPipeDestroyGrace, numEnvOr } from "./remote-shell.js";
9
9
  import { buildLivenessCut, commandBoundsOf } from "../exec-bounds.js";
10
+ import { buildCapturedOutput, createCutError } from "../exec-cut.js";
10
11
  import { hostBackgroundShellEnabled, hostExecSpoolEnabled } from "../config.js";
11
12
  import { IS_WIN32, resolveHostShell, hostShell, pinHostShell, spawnGroupOptions, killTreeHard, killTreeSoft, hardKillGroupNow, collapseWin32EnvKeys, adoptHostForegroundChild } from "./host-platform.js";
12
13
  import { BackgroundShellManager, seedMemStream, feedMemStream, drainMemStream } from "./background-shell-support.js";
13
14
  import { FileError, NodeExecutionEnv, inheritEnvPolicyProblem, ExecutionError, RemoteExecutionError, RollingTailBuffer, walkSymlinkChain, markTruncated, SchedulerError, BackgroundShellError, } from "@sema-agent/core";
15
+ import { capabilitiesFor } from "../execution-lane-caps.js";
16
+ import { LaneHomeSeat } from "../lane-home-probe.js";
14
17
  import { composeHostSubprocessEnv, hostSubprocessEnv, withheldFromHostSubprocess } from "../server-secret-env.js";
15
18
  import { nameTooLongCode } from "./remote-env-file-error.js";
16
19
  import { trackEphemeralHostWorkspace, untrackEphemeralHostWorkspace } from "./host-workspace-registry.js";
@@ -37,7 +40,7 @@ const UNSUPPORTED_SCHEDULER_CAPS = {
37
40
  maxScheduledHorizonSec: 0,
38
41
  };
39
42
  export class RemoteHostExecutionEnv {
40
- capabilities = { isolation: false, suspendable: false, canonicalPathAuthoritative: true, sharesProcessTable: true };
43
+ capabilities = capabilitiesFor({ remoteExec: { provider: "host" } });
41
44
  get schedulerCapabilities() {
42
45
  return this.cfg.scheduler?.schedulerCapabilities ?? UNSUPPORTED_SCHEDULER_CAPS;
43
46
  }
@@ -57,7 +60,11 @@ export class RemoteHostExecutionEnv {
57
60
  : Promise.resolve({ ok: false, error: new SchedulerError("unsupported", "scheduler not enabled on this worker") });
58
61
  }
59
62
  cwd;
60
- homeDir;
63
+ get homeDir() {
64
+ return this.homeSeat.home;
65
+ }
66
+ homeSeat;
67
+ homeRefusal;
61
68
  get cdPath() {
62
69
  const value = this.childEnv(undefined, undefined).CDPATH;
63
70
  return value === undefined || value === "" ? [] : value.split(":");
@@ -139,8 +146,9 @@ export class RemoteHostExecutionEnv {
139
146
  if (Array.isArray(config.inheritEnv))
140
147
  this.cfg.inheritEnv = [...config.inheritEnv];
141
148
  this.cwd = this.workspaceDir;
142
- this.homeDir = config.homeDir;
143
149
  this.handle = { sandboxId: this.workspaceDir, provider: PROVIDER, mountPath: this.workspaceDir };
150
+ this.homeSeat = new LaneHomeSeat(PROVIDER, config, IS_WIN32 ? "win32" : "posix");
151
+ this.homeRefusal = this.homeSeat.settle(this.childHome())?.refusal;
144
152
  }
145
153
  workspaceHandle() {
146
154
  return this.handle;
@@ -150,6 +158,8 @@ export class RemoteHostExecutionEnv {
150
158
  return unsupported("connect(snapshotId)");
151
159
  if (this.destroyed)
152
160
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env already destroyed") };
161
+ if (this.homeRefusal !== undefined)
162
+ return { ok: false, error: new RemoteExecutionError("connect_failed", this.homeRefusal) };
153
163
  if (config?.abortSignal?.aborted)
154
164
  return { ok: false, error: new RemoteExecutionError("aborted", "connect aborted") };
155
165
  try {
@@ -454,11 +464,11 @@ export class RemoteHostExecutionEnv {
454
464
  };
455
465
  const timer = setTimeout(() => {
456
466
  kill();
457
- finish({ ok: false, error: new ExecutionError("timeout", `host exec exceeded ${timeoutMs}ms`) });
467
+ finish({ ok: false, error: createCutError({ code: "timeout", message: `host exec exceeded ${timeoutMs}ms` }, buildCapturedOutput(outBuf, errBuf)) });
458
468
  }, timeoutMs);
459
469
  const onAbort = () => {
460
470
  kill();
461
- finish({ ok: false, error: new ExecutionError("aborted", "host exec aborted") });
471
+ finish({ ok: false, error: createCutError({ code: "aborted", message: "host exec aborted" }, buildCapturedOutput(outBuf, errBuf)) });
462
472
  };
463
473
  if (options?.abortSignal)
464
474
  options.abortSignal.addEventListener("abort", onAbort, { once: true });
@@ -607,32 +617,36 @@ export class RemoteHostExecutionEnv {
607
617
  }
608
618
  catch { }
609
619
  };
610
- const feed = (lane, inc) => {
620
+ const record = (lane, inc) => {
611
621
  lane.buf.push(inc);
612
- if (!settled) {
613
- try {
614
- lane.cb?.(inc.toString("utf8"));
615
- }
616
- catch { }
622
+ };
623
+ const deliver = (lane, inc) => {
624
+ try {
625
+ lane.cb?.(inc.toString("utf8"));
617
626
  }
627
+ catch { }
618
628
  };
619
- const pumpOnce = () => {
629
+ const feed = (lane, inc) => {
630
+ record(lane, inc);
631
+ deliver(lane, inc);
632
+ };
633
+ const pumpOnce = (take) => {
620
634
  for (const lane of lanes) {
621
635
  skipAhead(lane, ROTATE_BYTES);
622
636
  const inc = readIncrement(lane.path, lane.key);
623
637
  if (inc)
624
- feed(lane, inc);
638
+ take(lane, inc);
625
639
  reclaim(lane);
626
640
  }
627
641
  };
628
- const drainFinal = () => {
642
+ const drainFinal = (take) => {
629
643
  for (const lane of lanes) {
630
644
  skipAhead(lane, FINAL_TAIL_BYTES);
631
645
  for (let i = 0; i < 4; i++) {
632
646
  const inc = readIncrement(lane.path, lane.key);
633
647
  if (!inc)
634
648
  break;
635
- feed(lane, inc);
649
+ take(lane, inc);
636
650
  }
637
651
  }
638
652
  };
@@ -661,15 +675,17 @@ export class RemoteHostExecutionEnv {
661
675
  }
662
676
  };
663
677
  let removeDetachListener;
664
- const finish = (r) => {
678
+ const finish = (outcome) => {
665
679
  if (settled)
666
- return;
680
+ return false;
667
681
  settled = true;
682
+ const r = outcome();
668
683
  clearTimeout(timer);
669
684
  options?.abortSignal?.removeEventListener("abort", onAbort);
670
685
  removeDetachListener?.();
671
686
  cleanupSpool();
672
687
  resolve(r);
688
+ return true;
673
689
  };
674
690
  const closeSpoolFds = () => {
675
691
  for (const fd of spool.fds) {
@@ -697,66 +713,56 @@ export class RemoteHostExecutionEnv {
697
713
  ticket = adoptHostForegroundChild(child);
698
714
  closeSpoolFds();
699
715
  child.on("error", (e) => {
700
- finish({ ok: false, error: new ExecutionError("spawn_error", `host spawn failed: ${e.message}`) });
716
+ finish(() => ({ ok: false, error: new ExecutionError("spawn_error", `host spawn failed: ${e.message}`) }));
701
717
  });
702
- const partial = () => {
703
- pumpOnce();
704
- const o = outBuf.result();
705
- const e = errBuf.result();
706
- return { stdout: markTruncated(o.text, o.droppedBytes), stderr: markTruncated(e.text, e.droppedBytes) };
718
+ const captured = () => {
719
+ pumpOnce(record);
720
+ return buildCapturedOutput(outBuf, errBuf);
707
721
  };
708
722
  let exited = false;
709
- const timer = setTimeout(() => {
723
+ const cut = (c) => {
710
724
  if (exited)
711
725
  return;
712
- try {
713
- if (child.pid != null)
714
- killTreeHard(child.pid);
715
- }
716
- catch {
726
+ finish(() => {
717
727
  try {
718
- child.kill("SIGKILL");
728
+ if (child.pid != null)
729
+ killTreeHard(child.pid);
719
730
  }
720
- catch { }
721
- }
722
- const p = partial();
723
- finish({ ok: false, error: new ExecutionError("timeout", `host exec exceeded ${timeoutMs}ms; partial stdout: ${p.stdout.slice(-2048)}`) });
724
- }, timeoutMs);
725
- const onAbort = () => {
726
- if (exited)
727
- return;
728
- try {
729
- if (child.pid != null)
730
- killTreeHard(child.pid);
731
- }
732
- catch {
733
- try {
734
- child.kill("SIGKILL");
731
+ catch {
732
+ try {
733
+ child.kill("SIGKILL");
734
+ }
735
+ catch { }
735
736
  }
736
- catch { }
737
- }
738
- finish({ ok: false, error: new ExecutionError("aborted", "host exec aborted") });
737
+ return { ok: false, error: createCutError(c, captured()) };
738
+ });
739
739
  };
740
+ const timer = setTimeout(() => cut({ code: "timeout", message: `host exec exceeded ${timeoutMs}ms` }), timeoutMs);
741
+ const onAbort = () => cut({ code: "aborted", message: "host exec aborted" });
740
742
  if (options?.abortSignal?.aborted) {
741
743
  onAbort();
742
744
  return;
743
745
  }
744
746
  if (options?.abortSignal)
745
747
  options.abortSignal.addEventListener("abort", onAbort, { once: true });
746
- pump = setInterval(pumpOnce, PUMP_MS);
748
+ pump = setInterval(() => pumpOnce(feed), PUMP_MS);
747
749
  pump.unref?.();
748
750
  const onDetach = () => {
749
751
  if (settled || exited)
750
752
  return;
751
- pumpOnce();
753
+ pumpOnce(feed);
754
+ if (settled)
755
+ return;
752
756
  const fgDropped = spoolRemoved.out + spoolRemoved.err;
753
757
  const shellId = this.adoptRunningExecSpool(child, spool, { out: 0, err: 0 }, fgDropped);
754
758
  if (shellId === undefined)
755
759
  return;
756
760
  handedOver = true;
757
- const o = outBuf.result();
758
- const e = errBuf.result();
759
- finish(ok({ stdout: markTruncated(o.text, o.droppedBytes), stderr: markTruncated(e.text, e.droppedBytes), exitCode: 0, detached: { shellId } }));
761
+ finish(() => {
762
+ const o = outBuf.result();
763
+ const e = errBuf.result();
764
+ return ok({ stdout: markTruncated(o.text, o.droppedBytes), stderr: markTruncated(e.text, e.droppedBytes), exitCode: 0, detached: { shellId } });
765
+ });
760
766
  };
761
767
  if (detachSignal) {
762
768
  if (detachSignal.aborted)
@@ -767,13 +773,20 @@ export class RemoteHostExecutionEnv {
767
773
  }
768
774
  }
769
775
  const settleFromTermination = (code, signal) => {
770
- if (settled)
771
- return;
772
- drainFinal();
773
- const o = outBuf.result();
774
- const e = errBuf.result();
775
776
  const exitCode = code ?? (signal ? 128 + (signalNumber(signal) ?? 0) : 1);
776
- finish(ok({ stdout: markTruncated(o.text, o.droppedBytes), stderr: markTruncated(e.text, e.droppedBytes), exitCode }));
777
+ const drained = [];
778
+ const won = finish(() => {
779
+ drainFinal((lane, inc) => {
780
+ record(lane, inc);
781
+ drained.push({ lane, inc });
782
+ });
783
+ const o = outBuf.result();
784
+ const e = errBuf.result();
785
+ return ok({ stdout: markTruncated(o.text, o.droppedBytes), stderr: markTruncated(e.text, e.droppedBytes), exitCode });
786
+ });
787
+ if (won)
788
+ for (const d of drained)
789
+ deliver(d.lane, d.inc);
777
790
  };
778
791
  child.on("close", (code, signal) => {
779
792
  if (drainTimer) {
@@ -972,13 +985,13 @@ export class RemoteHostExecutionEnv {
972
985
  lines = lines.slice(0, options.maxLines);
973
986
  return ok(lines);
974
987
  }
975
- boundReader;
988
+ coreFs;
976
989
  async openBound(p, abortSignal) {
977
990
  const ready = await this.fsReady(p);
978
991
  if (!ready.ok)
979
992
  return ready;
980
- this.boundReader ??= new NodeExecutionEnv({ cwd: this.workspaceDir });
981
- return this.boundReader.openBound(this.resolve(p), abortSignal);
993
+ this.coreFs ??= new NodeExecutionEnv({ cwd: this.workspaceDir });
994
+ return this.coreFs.openBound(this.resolve(p), abortSignal);
982
995
  }
983
996
  async readBinaryFile(p, abortSignal) {
984
997
  const ready = await this.fsReady(p);
@@ -998,16 +1011,8 @@ export class RemoteHostExecutionEnv {
998
1011
  const ready = await this.fsReady(p);
999
1012
  if (!ready.ok)
1000
1013
  return ready;
1001
- const abs = this.resolve(p);
1002
- try {
1003
- await fs.mkdir(path.dirname(abs), { recursive: true });
1004
- const data = typeof content === "string" ? content : Buffer.from(content);
1005
- await fs.writeFile(abs, data, { signal: abortSignal });
1006
- return ok(undefined);
1007
- }
1008
- catch (e) {
1009
- return { ok: false, error: this.fileError(e, abs, abortSignal) };
1010
- }
1014
+ this.coreFs ??= new NodeExecutionEnv({ cwd: this.workspaceDir });
1015
+ return this.coreFs.writeFile(this.resolve(p), content, abortSignal);
1011
1016
  });
1012
1017
  }
1013
1018
  appendFile(p, content, abortSignal) {
@@ -1257,6 +1262,8 @@ export class RemoteHostExecutionEnv {
1257
1262
  async shellReady() {
1258
1263
  if (this.destroyed)
1259
1264
  return { ok: false, error: new ExecutionError("shell_unavailable", "execution env already destroyed") };
1265
+ if (this.homeRefusal !== undefined)
1266
+ return { ok: false, error: new ExecutionError("shell_unavailable", this.homeRefusal) };
1260
1267
  try {
1261
1268
  try {
1262
1269
  await resolveHostShell();
@@ -1288,6 +1295,8 @@ export class RemoteHostExecutionEnv {
1288
1295
  return (this.spoolRootDir !== undefined && inside(this.spoolRootDir)) || (this.persistent && inside(this.workspaceDir));
1289
1296
  }
1290
1297
  async fsReady(p, opts) {
1298
+ if (this.homeRefusal !== undefined)
1299
+ return { ok: false, error: new FileError("target_unavailable", this.homeRefusal, this.resolve(p)) };
1291
1300
  if (this.destroyed) {
1292
1301
  const lingering = opts?.lingeringBgWrite === true && this._bgManager?.hasShells() === true && this.isLingeringMirrorTarget(p);
1293
1302
  if (!lingering)
@@ -1317,6 +1326,16 @@ export class RemoteHostExecutionEnv {
1317
1326
  pinHostShell(base, spawnShell);
1318
1327
  return collapseWin32EnvKeys(base);
1319
1328
  }
1329
+ childHome() {
1330
+ try {
1331
+ const env = this.childEnv(undefined, undefined);
1332
+ const home = Object.entries(env).find(([k]) => (IS_WIN32 ? k.toUpperCase() : k) === "HOME")?.[1];
1333
+ return { ok: true, value: home ?? os.userInfo().homedir };
1334
+ }
1335
+ catch (e) {
1336
+ return { ok: false, reason: errMsg(e) };
1337
+ }
1338
+ }
1320
1339
  fileError(e, p, abortSignal) {
1321
1340
  if (e instanceof FileError)
1322
1341
  return e;
@@ -2,7 +2,7 @@ import { type SpaceReclaimer, type WorkspaceSpacePolicy, type CacheReclaimReason
2
2
  import { type SpaceCommitFence } from "../workspace-space-commit.js";
3
3
  import { type CacheMissMemory, type WorkspaceCacheCapability } from "../workspace-cache.js";
4
4
  import { type SandboxEgressGap, type SandboxIpv6 } from "../sandbox-egress-policy.js";
5
- import { FileError, ExecutionError, RemoteExecutionError, type ExecutionEnv, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type SymlinkChain, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type VmLifecycleOptions, type ExecutionEnvFactory, type BackgroundShellCapability, type BackgroundShellId, type BackgroundShellError, type BackgroundSpawnOptions, type BackgroundPoll } from "@sema-agent/core";
5
+ import { FileError, ExecutionError, RemoteExecutionError, type ExecutionEnv, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type SymlinkChain, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type VmLifecycleOptions, type ExecutionEnvFactory, type BackgroundShellCapability, type BackgroundShellId, type BackgroundShellError, type BackgroundSpawnOptions, type BackgroundPoll, type ExecutionEnvCapabilities } from "@sema-agent/core";
6
6
  import type { PodSpecPatch } from "./k8s-exec-protocol.js";
7
7
  import { type LaneHomeConfig } from "../lane-home-probe.js";
8
8
  type ExecOpts = Parameters<ExecutionEnv["exec"]>[1];
@@ -16,7 +16,9 @@ export interface K8sEnvConfig extends LaneHomeConfig {
16
16
  caCert?: string;
17
17
  /** Skip TLS verification (lab only — never production). */
18
18
  insecureTls?: boolean;
19
- namespace?: string;
19
+ /** The namespace the sandbox pods live in (env `K8S_NAMESPACE`). REQUIRED, no default: the deployment declares it, and every reader
20
+ * (the adapter's API paths, the reaper's pod face, the boot's egress reading) reads this one field. */
21
+ namespace: string;
20
22
  /** Sandbox pod image (a plain docker image present in the cluster). REQUIRED. */
21
23
  image: string;
22
24
  /** RuntimeClass for the VM boundary. Default "kata-qemu" (set "" to run plain runc — tests/dev only). */
@@ -258,8 +260,6 @@ export { execSocketTlsOptions } from "./k8s-exec-protocol.js";
258
260
  * String() is "[object ErrorEvent]" — that masked the real TLS failure for a whole drill. */
259
261
  export declare function errorMessageOf(e: unknown): string;
260
262
  export { exitCodeFromStatus } from "./k8s-exec-protocol.js";
261
- /** The sandbox namespace when `K8S_NAMESPACE` is unset (the one spelling: the adapter and the boot's egress line read it). */
262
- export declare const K8S_SANDBOX_NAMESPACE_DEFAULT = "sandboxes";
263
263
  /**
264
264
  * The boot reading of the sandbox Secret seat, a closed set: `granted` — every verb allowed · `denied` — the API answered "not allowed" for these
265
265
  * verbs (a definite answer; it wins over a verb that could not be asked) · `unverified` — some verb could not be asked (the request failed, the
@@ -290,23 +290,8 @@ export type SandboxLeaseWrite = {
290
290
  readonly message: string;
291
291
  };
292
292
  export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, BackgroundShellCapability {
293
- /**
294
- * 🔴 **[ref] / core 7.20.0([ref])`canonicalPathAuthoritative: true`** —— 本适配器的 `canonicalPath`
295
- * 就是**命令真正跑的那块文件系统**的权威答案(`posix-shell-fs` 的 `canonicalPath` 原语,经 pod exec 解到落点)。声明它 = 告诉引擎:整链答不出来是**解析器出了事**
296
- * (RPC 断、传输掉),不是「一个它看不见的命名空间」⇒ 读边界的执行期复核**拒**,而不是让词法判决站着。
297
- * 缺席是今天每个适配器的读法(整链沉默 ⇒ 词法判决成立、命令照跑),所以这一声明是**收紧**方向。
298
- * 严格读法 `=== true`(core `canonicalPathIsAuthoritative`);`isSuspendable` 一族同样严格,对本仓的
299
- * 布尔字面量零影响(四键都写死字面 `true`/`false`,不存在「非布尔值被读成缺席」那一形 —— 那一形由
300
- * core 的 `config.execution_env_capability_invalid` 普查响亮播报,本仓的受众表已登记)。
301
- * 谁该声明由闭集门看着(`test/execution-lane-caps.test.ts` 的逐 lane 对账:新 lane 不表态 = 编译红)。
302
- */
303
- /** `sharesProcessTable`(core 7.57.0,必填):命令跑在沙箱 pod 自己的 PID 命名空间里(不开 hostPID),不在本进程所在的进程表。 */
304
- readonly capabilities: {
305
- isolation: boolean;
306
- suspendable: boolean;
307
- canonicalPathAuthoritative: true;
308
- sharesProcessTable: false;
309
- };
293
+ /** 能力块的唯一作者 = `capabilitiesFor`(`execution-lane-caps.ts`;逐键表态与理由在那里,本类零能力字面量)。快照店是它的输入(构造期铸,见下)。 */
294
+ readonly capabilities: ExecutionEnvCapabilities;
310
295
  cwd: string;
311
296
  private podName?;
312
297
  /** Cluster-internal pod address, captured when the pod reaches Running — the LSP `getHost` surface.
@@ -331,9 +316,9 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
331
316
  * the space restore (`latest` names an archive that is there and did not come back — {@link restoreSnapshot}) and the pod's admission (the cluster created
332
317
  * the pod and it ended `Failed` before it ran — {@link createPod}). Neither goes away by asking again: the same request answers the same. */
333
318
  private connectRefusal?;
334
- /** core `ExecutionEnv.homeDir`([ref]):pod 里执行用户的 home —— 声明胜,否则 pod 就绪那一拍实测的 `$HOME`,见 {@link LaneHomeSeat}。读时才取(没声明时 pod 起来之前缺席)。 */
319
+ /** core `ExecutionEnv.homeDir`([ref]):pod 里执行用户的 home —— 声明在场 ⇒ 声明(开环境那一拍与 `$HOME` 对证过),否则 pod 就绪那一拍实测的 `$HOME`,见 {@link LaneHomeSeat}。读时才取(没声明时 pod 起来之前缺席)。 */
335
320
  get homeDir(): string | undefined;
336
- /** 家目录的座(声明胜 · 否则 pod 就绪那一拍实测 · 实测缺席 ⇒ 缺席 + 一行 warn)。本车道对声明对证(与 local-docker / ssh 同一只判官、同一句拒)。 */
321
+ /** 家目录的座(声明在场 ⇒ 开环境那一拍对证 · 否则 pod 就绪那一拍实测 · 实测缺席 ⇒ 缺席 + 一行 warn)。与其余读得出家目录的车道同一只判官、同一句拒。 */
337
322
  private readonly homeSeat;
338
323
  /** core 7.49.0 [ref]:shell 子进程真拿到的 `CDPATH` 条目 —— 本车道的命令跑在 pod 里(镜像与 pod spec 的 env),本类不读那份 env ⇒ `"unknown"`(core 契约:说不出就说 unknown,
339
324
  * 不许报一张没读过的空表;删除守卫因此对裸 `cd <相对名>` 之后的删除问人,而不是按「没有 CDPATH」放行)。 */
@@ -813,19 +798,18 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
813
798
  maxLines?: number;
814
799
  abortSignal?: AbortSignal;
815
800
  }): Promise<Result<string[], FileError>>;
816
- /** 父目录自建(与 host/local-docker/adb/ssh 同口径)。抽成共用是因为 `appendFile` 曾**跳过**它:
817
- * 🔴 2026-07-25 实测的跨腿分叉 —— `appendFile` 直接调 `writeChunked`,整条路上没有 `mkdir -p`,于是
818
- * 「直接 append 建一个新日志路径」在 k8s 腿上返 `not_found`,而其余 5 腿都成功(host/local-docker/adb 实测,
819
- * ssh/e2b 由方法体判据)。core 的后台输出镜像正是"每拍 append 一次"的形,这条分叉会让它在 k8s 腿上从第一拍
820
- * 就失败,而 `mirrorFailed` 只会表现成一句「output file is INCOMPLETE」。 */
821
- private ensureParentDir;
822
801
  writeFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
802
+ /** 暂存写的两只面(规则与提交脚本的唯一属主 = `remote-shell.ts`;父目录与私有暂存目录由编排的准备那一步建):上传 = 分块整份写进私有目录里的暂存件;
803
+ * 提交 = 文件面探针 exec。两者同一身份(同一只容器里的 exec)。 */
804
+ private stagedLane;
823
805
  appendFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
824
806
  /**
825
807
  * Write `content` to `abs` via argv-embedded base64, chunked at WRITE_CHUNK_BYTES (no stdin on the exec
826
808
  * channel we use; v4 has no per-channel close). `truncateFirst` → the FIRST chunk uses `>` (overwrite),
827
809
  * the rest `>>` (append); `false` → all chunks `>>`. Empty content still runs exactly one command so an
828
810
  * empty file is created (write) / left intact (append). Shared by writeFile/appendFile (council DESIGN#2).
811
+ * Both file faces point this at a STAGING name (`writeFileStaged` / `appendFileStaged`), so a cut mid-way never reaches
812
+ * the target; the direct uses are the fallbacks of both faces (writeFile passes truncateFirst, appendFile does not) when the target's directory refuses a new name or the private staging directory could not be made private.
829
813
  */
830
814
  private writeChunked;
831
815
  private posixFsInst?;
@@ -4,7 +4,7 @@ import https from "node:https";
4
4
  import http from "node:http";
5
5
  import { StringDecoder } from "node:string_decoder";
6
6
  import WebSocket from "ws";
7
- import { buildCwdEnvCommand, createCwdGuardedCall, shellQuote, kindFromMode, numEnvOr } from "./remote-shell.js";
7
+ import { appendFileStaged, buildCwdEnvCommand, createCwdGuardedCall, shellQuote, kindFromMode, numEnvOr, writeFileStaged } from "./remote-shell.js";
8
8
  import { presignS3Url, presignS3ListUrl } from "./s3-presign.js";
9
9
  import { WORKSPACE_SNAPSHOT_BUCKET_DEFAULT } from "../object-store-buckets.js";
10
10
  import { SNAPSHOT_BUCKET_MISSING, isNoSuchBucketAnswer } from "./s3-answer.js";
@@ -15,8 +15,10 @@ import { WORKSPACE_CACHE_SPACE_LABEL, workspaceCacheClaimOf, workspaceCacheEnv }
15
15
  import { recordFailOpen } from "../observability/fail-open.js";
16
16
  import { PodDeleteRefused, reportPodDeleteFailed } from "./k8s-pod-delete-failure.js";
17
17
  import { buildLivenessCut, commandBoundsOf, laneLivenessOf, LIVENESS_MS_DEFAULT } from "../exec-bounds.js";
18
+ import { buildCapturedOutput, createCutError } from "../exec-cut.js";
18
19
  import { SANDBOX_IPV6_DEFAULT, sandboxEgressGaps, sandboxPodSysctls } from "../sandbox-egress-policy.js";
19
20
  import { FileError, ExecutionError, RemoteExecutionError, withRetry, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
21
+ import { capabilitiesFor } from "../execution-lane-caps.js";
20
22
  import { fileErrorFromExec, classifyFsStderr } from "./remote-env-file-error.js";
21
23
  import { createPosixShellFs } from "./posix-shell-fs.js";
22
24
  import { randomUUID } from "node:crypto";
@@ -171,7 +173,6 @@ export function errorMessageOf(e) {
171
173
  return typeof m === "string" && m ? m : String(e);
172
174
  }
173
175
  export { exitCodeFromStatus } from "./k8s-exec-protocol.js";
174
- export const K8S_SANDBOX_NAMESPACE_DEFAULT = "sandboxes";
175
176
  const SANDBOX_SECRET_NAME_PREFIX = "sbx-env-";
176
177
  const SANDBOX_SECRET_LABEL = "sema.io/sandbox-secret";
177
178
  const secretEnvKey = (name) => `env.${name}`;
@@ -235,7 +236,7 @@ export class RemoteK8sExecutionEnv {
235
236
  token: config.token,
236
237
  caCert: config.caCert,
237
238
  insecureTls: config.insecureTls ?? false,
238
- namespace: config.namespace ?? K8S_SANDBOX_NAMESPACE_DEFAULT,
239
+ namespace: config.namespace,
239
240
  image: config.image,
240
241
  runtimeClass: config.runtimeClass ?? "kata-qemu",
241
242
  mountPath: config.mountPath ?? K8S_WORKSPACE_MOUNT_DEFAULT,
@@ -258,7 +259,7 @@ export class RemoteK8sExecutionEnv {
258
259
  ...(config.snapshotExclude && config.snapshotExclude.length > 0 ? { snapshotExclude: config.snapshotExclude } : {}),
259
260
  };
260
261
  this.cwd = this.cfg.mountPath;
261
- this.homeSeat = new LaneHomeSeat({ lane: PROVIDER, declared: config.homeDir, verifiesDeclared: true, onAbsent: config.onHomeAbsent });
262
+ this.homeSeat = new LaneHomeSeat(PROVIDER, config);
262
263
  this.deps = deps ?? {};
263
264
  this.laneLiveness = laneLivenessOf(this.cfg.livenessMs, "K8S_LIVENESS_MS");
264
265
  const occupant = occupantKeyOf(config.spaceOccupant);
@@ -268,7 +269,7 @@ export class RemoteK8sExecutionEnv {
268
269
  this.logger = config.logger;
269
270
  this.onWorkspaceSnapshot = config.onWorkspaceSnapshot;
270
271
  this.cacheMisses = config.cacheMisses;
271
- this.capabilities = { isolation: true, suspendable: !!this.cfg.s3Snapshot, canonicalPathAuthoritative: true, sharesProcessTable: false };
272
+ this.capabilities = capabilitiesFor({ remoteExec: { provider: PROVIDER, s3Snapshot: this.cfg.s3Snapshot } });
272
273
  const bgMax = Math.max(1, Math.min(K8S_BG_MAX_TIMEOUT_SEC, Math.floor(this.cfg.timeoutMs / 1000)));
273
274
  this.backgroundCapabilities = {
274
275
  supported: true,
@@ -1498,10 +1499,11 @@ export class RemoteK8sExecutionEnv {
1498
1499
  let sinkClosed = false;
1499
1500
  let removeDetachListener;
1500
1501
  const detachTee = opts?.detach?.tee;
1501
- const finish = (r) => {
1502
+ const finish = (outcome) => {
1502
1503
  if (settled)
1503
- return;
1504
+ return false;
1504
1505
  settled = true;
1506
+ const r = outcome();
1505
1507
  clearTimeout(wallTimer);
1506
1508
  clearTimeout(livenessTimer);
1507
1509
  opts?.signal?.removeEventListener("abort", onAbort);
@@ -1512,11 +1514,37 @@ export class RemoteK8sExecutionEnv {
1512
1514
  catch {
1513
1515
  }
1514
1516
  resolvePromise(r);
1517
+ return true;
1515
1518
  };
1516
1519
  const bounds = commandBoundsOf({ timeoutMs: opts?.timeoutMs, readTimeoutMs: opts?.readTimeoutMs }, this.laneLiveness);
1517
1520
  const capMs = opts?.capMs;
1518
1521
  const wallMs = bounds.wallMs === undefined ? capMs : capMs === undefined ? bounds.wallMs : Math.min(bounds.wallMs, capMs);
1519
- const wallTimer = wallMs !== undefined ? setTimeout(() => finish({ ok: false, error: new ExecutionError("timeout", `command exceeded ${wallMs}ms`) }), wallMs) : undefined;
1522
+ const recordStderr = (bytes) => {
1523
+ if (bytes.length === 0)
1524
+ return;
1525
+ errBuf.push(bytes);
1526
+ if (detachTee)
1527
+ feedMemStream(detachTee.err, bytes, K8S_BG_MEM_CAP);
1528
+ };
1529
+ const emitStderr = (bytes) => {
1530
+ if (bytes.length > 0 && errDecoder)
1531
+ opts.onStderr(errDecoder.write(bytes));
1532
+ };
1533
+ const takeStderr = (bytes) => {
1534
+ recordStderr(bytes);
1535
+ emitStderr(bytes);
1536
+ };
1537
+ const heldStderr = () => opts?.stderrFilter?.endStderr();
1538
+ const captured = () => {
1539
+ const held = heldStderr();
1540
+ if (held)
1541
+ recordStderr(held);
1542
+ return buildCapturedOutput(outTail, errBuf);
1543
+ };
1544
+ const cut = (c) => {
1545
+ finish(() => ({ ok: false, error: createCutError(c, captured()) }));
1546
+ };
1547
+ const wallTimer = wallMs !== undefined ? setTimeout(() => cut({ code: "timeout", message: `command exceeded ${wallMs}ms` }), wallMs) : undefined;
1520
1548
  let livenessTimer;
1521
1549
  const liveness = bounds.liveness;
1522
1550
  const armLiveness = () => {
@@ -1525,28 +1553,18 @@ export class RemoteK8sExecutionEnv {
1525
1553
  clearTimeout(livenessTimer);
1526
1554
  const idle = Date.now() - lastProgress;
1527
1555
  if (idle >= liveness.ms) {
1528
- const cut = buildLivenessCut(liveness);
1529
- finish({ ok: false, error: new ExecutionError(cut.code, cut.message, undefined, undefined, cut.noOutputSec) });
1556
+ cut(buildLivenessCut(liveness));
1530
1557
  return;
1531
1558
  }
1532
1559
  livenessTimer = setTimeout(armLiveness, liveness.ms - idle);
1533
1560
  };
1534
1561
  armLiveness();
1535
- const onAbort = () => finish({ ok: false, error: new ExecutionError("aborted", "command aborted") });
1562
+ const onAbort = () => cut({ code: "aborted", message: "command aborted" });
1536
1563
  if (opts?.signal) {
1537
1564
  if (opts.signal.aborted)
1538
1565
  return onAbort();
1539
1566
  opts.signal.addEventListener("abort", onAbort, { once: true });
1540
1567
  }
1541
- const takeStderr = (bytes) => {
1542
- if (bytes.length === 0)
1543
- return;
1544
- errBuf.push(bytes);
1545
- if (detachTee)
1546
- feedMemStream(detachTee.err, bytes, K8S_BG_MEM_CAP);
1547
- if (errDecoder)
1548
- opts.onStderr(errDecoder.write(bytes));
1549
- };
1550
1568
  sock.on("message", (...args) => {
1551
1569
  const data = args[0];
1552
1570
  const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
@@ -1588,7 +1606,7 @@ export class RemoteK8sExecutionEnv {
1588
1606
  }
1589
1607
  return;
1590
1608
  }
1591
- finish({ ok: false, error: new ExecutionError("transport_lost", `exec socket error: ${errorMessageOf(e)}`, e instanceof Error ? e : undefined) });
1609
+ finish(() => ({ ok: false, error: new ExecutionError("transport_lost", `exec socket error: ${errorMessageOf(e)}`, e instanceof Error ? e : undefined) }));
1592
1610
  });
1593
1611
  sock.on("close", () => {
1594
1612
  if (spliceSink) {
@@ -1601,25 +1619,30 @@ export class RemoteK8sExecutionEnv {
1601
1619
  return spliceSink.closed("error" in parsed ? { failed: true } : { failed: false, exitCode: parsed.exitCode });
1602
1620
  }
1603
1621
  if (statusFrame === undefined) {
1604
- finish({ ok: false, error: new ExecutionError("transport_lost", "exec socket closed without status — k8s transport lost mid-command") });
1622
+ finish(() => ({ ok: false, error: new ExecutionError("transport_lost", "exec socket closed without status — k8s transport lost mid-command") }));
1605
1623
  return;
1606
1624
  }
1607
1625
  const parsed = exitCodeFromStatus(statusFrame);
1608
1626
  if ("error" in parsed) {
1609
- finish({ ok: false, error: new ExecutionError("spawn_error", parsed.error) });
1627
+ const said = parsed.error;
1628
+ finish(() => ({ ok: false, error: new ExecutionError("spawn_error", said) }));
1610
1629
  return;
1611
1630
  }
1612
- if (opts?.stderrFilter)
1613
- takeStderr(opts.stderrFilter.endStderr());
1614
- const err = errBuf.result();
1615
- const stderr = markTruncated(err.text, err.droppedBytes);
1616
- if (boundOutput) {
1617
- const out = outTail.result();
1618
- finish(ok({ stdout: markTruncated(out.text, out.droppedBytes), stderr, exitCode: parsed.exitCode }));
1619
- }
1620
- else {
1621
- finish(ok({ stdout: Buffer.concat(outBufs).toString("utf8"), stderr, exitCode: parsed.exitCode }));
1622
- }
1631
+ const exitCode = parsed.exitCode;
1632
+ const held = heldStderr();
1633
+ const won = finish(() => {
1634
+ if (held)
1635
+ recordStderr(held);
1636
+ const err = errBuf.result();
1637
+ const stderr = markTruncated(err.text, err.droppedBytes);
1638
+ if (boundOutput) {
1639
+ const out = outTail.result();
1640
+ return ok({ stdout: markTruncated(out.text, out.droppedBytes), stderr, exitCode });
1641
+ }
1642
+ return ok({ stdout: Buffer.concat(outBufs).toString("utf8"), stderr, exitCode });
1643
+ });
1644
+ if (won && held)
1645
+ emitStderr(held);
1623
1646
  });
1624
1647
  const detach = opts?.detach;
1625
1648
  if (detach) {
@@ -1925,30 +1948,15 @@ export class RemoteK8sExecutionEnv {
1925
1948
  lines = lines.slice(0, options.maxLines);
1926
1949
  return ok(lines);
1927
1950
  }
1928
- async ensureParentDir(abs, abortSignal) {
1929
- const dir = path.posix.dirname(abs);
1930
- if (!dir || dir === "." || dir === "/")
1931
- return undefined;
1932
- const mk = await this.fsExec(`mkdir -p ${shellQuote(dir)}`, abortSignal);
1933
- if (!mk.ok)
1934
- return { ok: false, error: fileErrorFromExec(mk.error, dir) };
1935
- if (mk.value.exitCode !== 0)
1936
- return { ok: false, error: classifyFsStderr(mk.value.stderr, dir) };
1937
- return undefined;
1938
- }
1939
1951
  async writeFile(p, content, abortSignal) {
1940
- const abs = this.resolve(p);
1941
- const bad = await this.ensureParentDir(abs, abortSignal);
1942
- if (bad)
1943
- return bad;
1944
- return this.writeChunked(abs, content, true, abortSignal);
1952
+ return writeFileStaged(this.stagedLane(content), this.resolve(p), abortSignal);
1953
+ }
1954
+ stagedLane(content) {
1955
+ return { upload: (dest, signal) => this.writeChunked(dest, content, true, signal), run: (cmd, signal) => this.fsExec(cmd, signal) };
1945
1956
  }
1946
1957
  async appendFile(p, content, abortSignal) {
1947
1958
  const abs = this.resolve(p);
1948
- const bad = await this.ensureParentDir(abs, abortSignal);
1949
- if (bad)
1950
- return bad;
1951
- return this.writeChunked(abs, content, false, abortSignal);
1959
+ return appendFileStaged(this.stagedLane(content), abs, abortSignal, (ctx) => ctx.inPlace((signal) => this.writeChunked(abs, content, false, signal)));
1952
1960
  }
1953
1961
  async writeChunked(abs, content, truncateFirst, abortSignal) {
1954
1962
  const bytes = typeof content === "string" ? Buffer.from(content, "utf8") : Buffer.from(content);
@@ -2119,7 +2127,7 @@ export function createSandboxPodApi(config, deps) {
2119
2127
  let env;
2120
2128
  const control = () => (env ??= new RemoteK8sExecutionEnv(config, deps));
2121
2129
  return {
2122
- namespace: config.namespace ?? K8S_SANDBOX_NAMESPACE_DEFAULT,
2130
+ namespace: config.namespace,
2123
2131
  list: () => control().listSandboxPods(),
2124
2132
  delete: (name) => control().deletePod(name),
2125
2133
  };