@sema-agent/server 7.43.0 → 7.44.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 (345) hide show
  1. package/USAGE.md +18 -0
  2. package/dist/adoption/plan.js +31 -192
  3. package/dist/adoption/quiesce.js +3 -86
  4. package/dist/adoption/runner.js +3 -140
  5. package/dist/adoption/sql.js +0 -74
  6. package/dist/adoption/wire.js +0 -80
  7. package/dist/approval-ask-machine.js +0 -75
  8. package/dist/approval-card.js +0 -323
  9. package/dist/approval-deny-reasons.js +0 -42
  10. package/dist/approval-hmac.js +0 -35
  11. package/dist/approval-reconciler.js +8 -166
  12. package/dist/approval.js +4 -67
  13. package/dist/audit.js +1 -44
  14. package/dist/auth-bridge.js +4 -47
  15. package/dist/auth-keys.js +0 -23
  16. package/dist/bake-runner/main.js +4 -65
  17. package/dist/bake-runner/protocol.js +3 -58
  18. package/dist/bake-runner/runner.js +5 -91
  19. package/dist/bench/l8/artifact.js +2 -61
  20. package/dist/bench/l8/escape.js +0 -25
  21. package/dist/bench/l8/index.js +0 -14
  22. package/dist/bench/l8/probes.js +0 -60
  23. package/dist/bench/l8/run-probes.js +2 -45
  24. package/dist/bench/s1/arms.js +14 -189
  25. package/dist/bench/s1/live-deps.js +11 -253
  26. package/dist/bench/s1/oracle.js +1 -36
  27. package/dist/bench/s1/repair-oracle-adapter.js +0 -30
  28. package/dist/bench/s1/reviewer.js +0 -34
  29. package/dist/bench/s1/row.js +0 -62
  30. package/dist/bench/s1/run-firm.js +4 -70
  31. package/dist/bench/s1/runner-ctx.js +0 -40
  32. package/dist/bench/s1/tasks.js +0 -115
  33. package/dist/boot/adoption.js +0 -20
  34. package/dist/boot/budget-tracing.js +3 -55
  35. package/dist/boot/config-center.js +43 -551
  36. package/dist/boot/coordinators.js +1 -69
  37. package/dist/boot/crash-last.js +0 -18
  38. package/dist/boot/deferred-sandbox-path-env.js +3 -119
  39. package/dist/boot/execution-env.js +5 -171
  40. package/dist/boot/governance-seams.js +0 -92
  41. package/dist/boot/leader.js +0 -69
  42. package/dist/boot/limit-sync.js +0 -2
  43. package/dist/boot/memory-boundary.js +3 -91
  44. package/dist/boot/org-memory.js +1 -25
  45. package/dist/boot/parked-revive-gate.js +0 -150
  46. package/dist/boot/permission-rules-audit.js +1 -56
  47. package/dist/boot/reapers.js +15 -273
  48. package/dist/boot/resolve-spec.js +9 -768
  49. package/dist/boot/retention-lane.js +4 -87
  50. package/dist/boot/runner-deps.js +3 -297
  51. package/dist/boot/runtime-caps.js +0 -40
  52. package/dist/boot/session-faces.js +2 -133
  53. package/dist/boot/shutdown.js +9 -83
  54. package/dist/boot/side-query-lane.js +2 -137
  55. package/dist/boot/stores.js +15 -215
  56. package/dist/boot/task-list-lane.js +0 -18
  57. package/dist/boot/webfetch-summarize-lane.js +0 -47
  58. package/dist/boot/workflow-orchestration.js +3 -99
  59. package/dist/boot-reclaim.js +0 -20
  60. package/dist/bounded-session-map.js +0 -19
  61. package/dist/brain.js +2 -139
  62. package/dist/budget.js +9 -244
  63. package/dist/capabilities/builtin-tools.js +0 -3
  64. package/dist/capabilities/center-plugins.js +6 -51
  65. package/dist/capabilities/center-prompts.js +6 -71
  66. package/dist/capabilities/code-review-council.js +7 -40
  67. package/dist/capabilities/collab-workflows.js +1 -44
  68. package/dist/capabilities/hands-lane.js +0 -65
  69. package/dist/capabilities/memory-notice.js +0 -70
  70. package/dist/capabilities/prompt.js +0 -14
  71. package/dist/capabilities/prompts/code-review.js +0 -14
  72. package/dist/capabilities/prompts/identity.js +0 -6
  73. package/dist/capabilities/prompts/team.js +0 -4
  74. package/dist/capabilities/repo-tools.js +0 -34
  75. package/dist/capabilities/sandbox-file-send.js +3 -92
  76. package/dist/capabilities/scenarios.d.ts +0 -1
  77. package/dist/capabilities/scenarios.js +18 -279
  78. package/dist/capabilities/select-environment-tool.js +0 -32
  79. package/dist/capabilities/send-user-file-tool.js +2 -66
  80. package/dist/capabilities/skills.d.ts +1 -2
  81. package/dist/capabilities/skills.js +3 -21
  82. package/dist/capabilities/team.d.ts +3 -11
  83. package/dist/capabilities/team.js +3 -51
  84. package/dist/capabilities/tool-defer.js +0 -3
  85. package/dist/config-center/apply-effective.js +15 -330
  86. package/dist/config-center/apply-ledger.js +5 -38
  87. package/dist/config-center/facade.js +0 -41
  88. package/dist/config-center/hot-keys-registry.js +0 -20
  89. package/dist/config-center/http-client.js +2 -125
  90. package/dist/config-center/mcp-revocation.js +2 -34
  91. package/dist/config-center/read-face.js +0 -59
  92. package/dist/config-center/restart-signal.js +1 -85
  93. package/dist/config-center/skills-mcp.d.ts +1 -1
  94. package/dist/config-center/skills-mcp.js +9 -76
  95. package/dist/config-center/stage-limits.js +8 -35
  96. package/dist/config-invariants.js +0 -16
  97. package/dist/config-lkg.js +0 -42
  98. package/dist/config-provider.js +3 -186
  99. package/dist/config-types.js +0 -5
  100. package/dist/config.js +107 -1145
  101. package/dist/degenerate-instrument.js +3 -67
  102. package/dist/deployment-governance.js +0 -124
  103. package/dist/digest-form.js +0 -12
  104. package/dist/elicitation.js +3 -86
  105. package/dist/env-facts.js +7 -75
  106. package/dist/fleet/fleet-bus.js +34 -507
  107. package/dist/fleet/fleet-reconciler.js +9 -149
  108. package/dist/fleet/fleet-terminal-window.js +10 -178
  109. package/dist/fleet/subagent-tail-bus.js +3 -72
  110. package/dist/fleet-client.js +10 -70
  111. package/dist/fleet-lease.js +5 -79
  112. package/dist/git-api-kind.js +0 -3
  113. package/dist/governance-ask-marks.js +2 -78
  114. package/dist/hooks/branch-transcript.js +0 -74
  115. package/dist/hooks/cc-agent-hook-prompt.js +0 -29
  116. package/dist/hooks/cc-stop-prompt.js +1 -46
  117. package/dist/hooks/hook-llm.js +1 -53
  118. package/dist/hooks/hook-runner.js +20 -414
  119. package/dist/http/active-run-conflict.js +4 -129
  120. package/dist/http/cursor-fingerprint.d.ts +5 -0
  121. package/dist/http/cursor-fingerprint.js +5 -0
  122. package/dist/http/idempotency.js +0 -37
  123. package/dist/http/principal-gate.js +3 -40
  124. package/dist/http/route-ctx.js +0 -9
  125. package/dist/http/routes/a2a-serve.js +7 -319
  126. package/dist/http/routes/admin-config-refresh.js +0 -4
  127. package/dist/http/routes/admin-drain.js +0 -10
  128. package/dist/http/routes/adoption.js +1 -29
  129. package/dist/http/routes/agents-roster.js +1 -47
  130. package/dist/http/routes/approvals-assistant.js +32 -395
  131. package/dist/http/routes/attachments.js +4 -22
  132. package/dist/http/routes/capabilities.js +3 -420
  133. package/dist/http/routes/diagnostics.js +1 -81
  134. package/dist/http/routes/fleet.js +9 -185
  135. package/dist/http/routes/images.js +14 -239
  136. package/dist/http/routes/leader.js +0 -13
  137. package/dist/http/routes/memory-bundle.js +1 -61
  138. package/dist/http/routes/memory-policy.js +9 -99
  139. package/dist/http/routes/notify-wake.js +3 -37
  140. package/dist/http/routes/observability.js +2 -19
  141. package/dist/http/routes/retention-ops.js +2 -34
  142. package/dist/http/routes/rules.js +1 -93
  143. package/dist/http/routes/runs.js +64 -873
  144. package/dist/http/routes/session-sync.js +19 -258
  145. package/dist/http/routes/sessions-list.js +17 -43
  146. package/dist/http/routes/sessions.js +34 -226
  147. package/dist/http/routes/shared-memory.js +5 -36
  148. package/dist/http/routes/side-query.js +1 -87
  149. package/dist/http/routes/tasks.js +52 -722
  150. package/dist/http/routes/trace-usage.js +32 -195
  151. package/dist/http/routes/workflows.js +19 -195
  152. package/dist/http/run-meta.js +0 -6
  153. package/dist/http/send.js +0 -32
  154. package/dist/http/server.js +92 -1597
  155. package/dist/http/sse-lifecycle.js +2 -13
  156. package/dist/http/sse-log.js +3 -48
  157. package/dist/http/tar.js +5 -21
  158. package/dist/http/verify-rounds.js +0 -5
  159. package/dist/http/wire-gate.js +0 -9
  160. package/dist/http/workspace-content.js +0 -10
  161. package/dist/images/bake-validate.js +1 -70
  162. package/dist/images/manifest.js +1 -6
  163. package/dist/index.js +0 -21
  164. package/dist/key-resolver.js +2 -17
  165. package/dist/leader/diffout.js +1 -20
  166. package/dist/leader/diffup.js +0 -47
  167. package/dist/leader/endpoint.js +2 -57
  168. package/dist/leader/fanout.js +3 -45
  169. package/dist/leader/grader-env-factory.js +3 -72
  170. package/dist/leader/leader.js +5 -156
  171. package/dist/leader/merge.js +8 -115
  172. package/dist/leader/planner.js +3 -54
  173. package/dist/leader/repair-oracle.js +1 -60
  174. package/dist/leader/repair-wire.js +2 -79
  175. package/dist/leader/wire.js +8 -307
  176. package/dist/lsp/e2b-bridge.js +4 -64
  177. package/dist/lsp/e2b-manager.js +6 -94
  178. package/dist/lsp/lsp-frames.js +0 -12
  179. package/dist/lsp/manager.js +4 -96
  180. package/dist/lsp/ws-transport.js +5 -55
  181. package/dist/lsp-evict.js +1 -15
  182. package/dist/main.js +57 -765
  183. package/dist/memory-bundle-engine.js +0 -55
  184. package/dist/memory-export.js +0 -4
  185. package/dist/memory-posture.js +1 -15
  186. package/dist/memory-scope.js +11 -148
  187. package/dist/memory-sync-client.js +2 -44
  188. package/dist/memory-sync.js +1 -80
  189. package/dist/model-select.js +3 -80
  190. package/dist/observability/cost-quota.js +1 -17
  191. package/dist/observability/cost-taxonomy.js +0 -34
  192. package/dist/observability/fail-open.js +7 -86
  193. package/dist/observability/logger.js +0 -6
  194. package/dist/observability/metrics.js +0 -94
  195. package/dist/observability/otel-exporter.js +3 -13
  196. package/dist/observability/principal-context.js +0 -9
  197. package/dist/observability/prompt-manifest.js +1 -37
  198. package/dist/observability/rate-limit.js +0 -4
  199. package/dist/observability/secret-env-scrub.js +2 -56
  200. package/dist/observability/tool-trace.js +1 -70
  201. package/dist/orchestration/hardened-vm-runner.js +4 -118
  202. package/dist/orchestration/hardened-vm-worker-runner.js +1 -26
  203. package/dist/orchestration/hardened-vm-worker.js +0 -27
  204. package/dist/orchestration/subagent-steer.js +1 -45
  205. package/dist/orchestration/workflow-agent-steer.js +1 -80
  206. package/dist/orchestration/workflow-completion-inbox.js +32 -285
  207. package/dist/orchestration/workflow-notify-journal.js +16 -259
  208. package/dist/org-memory-admission.js +3 -47
  209. package/dist/parent-watch.js +2 -48
  210. package/dist/parked-decide.js +1 -109
  211. package/dist/per-task-image.js +0 -57
  212. package/dist/plan-cache-probe.js +3 -27
  213. package/dist/plugins/adoption-log-sql.js +2 -119
  214. package/dist/plugins/approval-ask-store-memory.js +3 -38
  215. package/dist/plugins/approval-ask-store-sql.js +6 -188
  216. package/dist/plugins/approval-exemption-store.js +2 -28
  217. package/dist/plugins/background-agent-store-sql.js +4 -105
  218. package/dist/plugins/background-shell-support.js +14 -122
  219. package/dist/plugins/blob-backend.js +6 -169
  220. package/dist/plugins/breaker-state-sql.js +8 -46
  221. package/dist/plugins/caching-session-store.js +4 -106
  222. package/dist/plugins/checkpoint-store-sql.js +18 -505
  223. package/dist/plugins/e2b-orphan-reclaim.js +0 -45
  224. package/dist/plugins/file-outcome-sink.js +0 -9
  225. package/dist/plugins/file-resume-anchor-store.js +4 -43
  226. package/dist/plugins/file-run-store.js +26 -364
  227. package/dist/plugins/file-snapshot-store-sql.js +10 -181
  228. package/dist/plugins/fork-routing-session-store.js +8 -111
  229. package/dist/plugins/host-platform.js +2 -91
  230. package/dist/plugins/image-bake-store-sql.js +7 -250
  231. package/dist/plugins/image-index-sql.js +4 -123
  232. package/dist/plugins/k8s-bg-scripts.js +4 -88
  233. package/dist/plugins/k8s-exec-protocol.js +0 -41
  234. package/dist/plugins/leader-run-store-sql.js +0 -101
  235. package/dist/plugins/local-checkpoint-store.js +8 -128
  236. package/dist/plugins/local-session-store.js +34 -296
  237. package/dist/plugins/local-task-attachment-store.js +2 -16
  238. package/dist/plugins/mailbox-store-sql.js +8 -65
  239. package/dist/plugins/memory-embedder-fingerprint.js +5 -166
  240. package/dist/plugins/memory-embedder.js +4 -70
  241. package/dist/plugins/memory-engine-pg.js +6 -170
  242. package/dist/plugins/memory-engine-tidb.js +7 -155
  243. package/dist/plugins/memory-engine-vector-util.js +0 -10
  244. package/dist/plugins/memory-key-guards.js +0 -34
  245. package/dist/plugins/memory-origin-law.js +0 -187
  246. package/dist/plugins/memory-resume-anchor-store.js +0 -17
  247. package/dist/plugins/memory-run-store.js +12 -89
  248. package/dist/plugins/memory-session-policy-store.js +0 -17
  249. package/dist/plugins/memory-sync-store-pg.js +4 -49
  250. package/dist/plugins/memory-sync-store-tidb.js +3 -35
  251. package/dist/plugins/outcome-ledger-sql.js +3 -97
  252. package/dist/plugins/permission-rule-store-file.js +5 -133
  253. package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
  254. package/dist/plugins/permission-rule-store-sql.js +50 -307
  255. package/dist/plugins/pg-cost-quota.js +0 -7
  256. package/dist/plugins/pg-pool.js +0 -92
  257. package/dist/plugins/pg-rate-limiter.js +2 -13
  258. package/dist/plugins/pg-safe-json.js +4 -40
  259. package/dist/plugins/pg-session-storage.js +25 -189
  260. package/dist/plugins/posix-shell-fs.js +1 -31
  261. package/dist/plugins/remote-env-adb.js +12 -101
  262. package/dist/plugins/remote-env-e2b.js +40 -371
  263. package/dist/plugins/remote-env-file-error.js +0 -37
  264. package/dist/plugins/remote-env-host.js +63 -483
  265. package/dist/plugins/remote-env-k8s.js +32 -326
  266. package/dist/plugins/remote-env-local-docker.js +24 -157
  267. package/dist/plugins/remote-env-ssh.js +20 -128
  268. package/dist/plugins/remote-scratchpad.js +2 -32
  269. package/dist/plugins/remote-shell.js +1 -32
  270. package/dist/plugins/resume-anchor-store-sql.js +0 -11
  271. package/dist/plugins/retention-lane-store-sql.js +0 -108
  272. package/dist/plugins/retention-store-sql.js +4 -383
  273. package/dist/plugins/roster-store-sql.js +0 -55
  274. package/dist/plugins/run-store-sql.js +14 -251
  275. package/dist/plugins/s3-presign.js +2 -49
  276. package/dist/plugins/scheduler-support.js +3 -80
  277. package/dist/plugins/send-file-ledger.js +4 -53
  278. package/dist/plugins/send-user-file.js +4 -94
  279. package/dist/plugins/session-placement.js +1 -89
  280. package/dist/plugins/session-policy-store-sql.js +3 -81
  281. package/dist/plugins/session-store.js +0 -57
  282. package/dist/plugins/shared-memory-store-sql.js +4 -167
  283. package/dist/plugins/sql-driver.js +0 -17
  284. package/dist/plugins/sql-errors.js +0 -7
  285. package/dist/plugins/sql-escape.js +0 -8
  286. package/dist/plugins/sql-row-helpers.js +0 -25
  287. package/dist/plugins/store-backend.js +42 -222
  288. package/dist/plugins/store-contracts.js +2 -46
  289. package/dist/plugins/task-attachment-store.js +3 -49
  290. package/dist/plugins/task-list-store-sql.js +0 -79
  291. package/dist/plugins/tidb-cost-quota.js +1 -4
  292. package/dist/plugins/tidb-pool.js +1 -207
  293. package/dist/plugins/tidb-rate-limiter.js +3 -9
  294. package/dist/plugins/tidb-session-storage.js +4 -70
  295. package/dist/plugins/tidb-session-store.js +19 -341
  296. package/dist/plugins/tool-result-store-sql.js +4 -179
  297. package/dist/plugins/usage-window-store-sql.js +0 -8
  298. package/dist/plugins/web-search.js +10 -132
  299. package/dist/plugins/workflow-journal-store-sql.js +2 -58
  300. package/dist/plugins/workflow-run-store-sql.js +6 -91
  301. package/dist/plugins/worktree-isolation.js +6 -126
  302. package/dist/plugins/write-behind-counter.js +16 -75
  303. package/dist/principal-jwt.js +5 -60
  304. package/dist/project-memory.js +15 -146
  305. package/dist/prompts-domain-validate.js +1 -51
  306. package/dist/question.js +2 -111
  307. package/dist/resource-suspend.js +0 -18
  308. package/dist/router/route-orchestration.js +0 -77
  309. package/dist/rules-consent.d.ts +44 -5
  310. package/dist/rules-consent.js +75 -215
  311. package/dist/run-local.js +14 -381
  312. package/dist/runs.js +29 -617
  313. package/dist/runtime-caps-resolver.js +6 -132
  314. package/dist/runtime-governance.js +1 -232
  315. package/dist/sandbox-pkg-source.js +0 -37
  316. package/dist/sealed-key.js +3 -68
  317. package/dist/security.js +6 -291
  318. package/dist/session-leaf-bus.js +0 -32
  319. package/dist/session-sync-content.js +1 -69
  320. package/dist/session-sync-kernel.js +3 -60
  321. package/dist/session-sync.js +3 -66
  322. package/dist/session-titler.js +7 -41
  323. package/dist/session-watch.js +9 -73
  324. package/dist/shared-memory-scope-authorizer.js +0 -18
  325. package/dist/sighup-idle.js +1 -12
  326. package/dist/spec-fields.js +4 -143
  327. package/dist/store-live-probe.js +3 -49
  328. package/dist/task-a2a.js +1 -126
  329. package/dist/task-cwd.js +1 -103
  330. package/dist/task-mcp.js +1 -89
  331. package/dist/task-settings.js +10 -300
  332. package/dist/task-workflow.js +8 -75
  333. package/dist/tool-approval.d.ts +28 -1
  334. package/dist/tool-approval.js +70 -1430
  335. package/dist/trace/artifacts.js +5 -20
  336. package/dist/trace/engine-notice-wire.js +6 -138
  337. package/dist/trace/ledger-sink.js +7 -96
  338. package/dist/trace/project.js +4 -448
  339. package/dist/trace/redact.js +9 -77
  340. package/dist/turn-activity.js +1 -27
  341. package/dist/usage-analytics.js +4 -34
  342. package/dist/wall-clock-jump-guard.js +1 -68
  343. package/package.json +3 -3
  344. package/dist/capabilities/scenario-alias.d.ts +0 -27
  345. package/dist/capabilities/scenario-alias.js +0 -61
@@ -1,35 +1,3 @@
1
- /**
2
- * `RemoteK8sExecutionEnv` — the Kubernetes pod-sandbox implementation of core's `RemoteExecutionEnv` seam
3
- * (design/61). A PEER adapter to E2B/SSH/ADB (NOT chained). Use case: self-hosted sandboxes on our k3s with
4
- * **Kata Containers** (`runtimeClassName: kata-qemu`) — a real VM boundary (guest kernel ≠ host, proven in the
5
- * 2026-06-10 PoC) at ~2s startup, par with managed E2B.
6
- *
7
- * 🔴 Capabilities: `{ isolation: true, suspendable: false }` — Kata gives VM isolation, but
8
- * there is NO managed memory snapshot (E2B-style pause/resume). `suspendVM`/`resumeVM`/`postResumeInit` →
9
- * typed `"unsupported"` so durable suspend degrades honestly (checkpoint + cold rebuild, in-VM process state
10
- * like a language server is lost). True memory snapshots = a separate CRIU/Kata-snapshot project, not implied.
11
- *
12
- * Transport: the k8s REST API + the exec WebSocket (channel-multiplexed: 1-byte channel prefix per frame —
13
- * 1=stdout 2=stderr 3=status JSON) spoken directly over `ws` — deliberately NO `@kubernetes/client-node`
14
- * dependency (its exec path is the only thing we'd use; our liveness/timeout discipline plugs straight into a
15
- * raw socket, and the fault-injection seam stays trivial). Unlike `adb exec-out`, the status channel carries a
16
- * REAL exit code.
17
- *
18
- * 🔴 Subprotocol = `v5.channel.k8s.io` first, `v4` fallback (night-drill 2026-06-11): the apiserver's **v4**
19
- * WebSocket exec writer STALLS after the first ~6KB of a fast output burst (verified: the TCP socket stays
20
- * healthy/flowing, the apiserver just stops sending; v5 streams the same 1MB+ burst to completion). v5 (k8s
21
- * 1.29+) is what modern kubectl negotiates; the frame format is identical (it only adds a close-channel we
22
- * ignore), so offering both lets a v5 apiserver fix the stall while a v4-only one still works for small output.
23
- *
24
- * Timeouts follow the E2B class-fix taxonomy: control-plane RPCs get a short wall-clock
25
- * (`rpcTimeoutMs`); commands get a zero-progress LIVENESS bound (`livenessMs`) plus the caller's explicit
26
- * wall-clock; pod readiness gets its own generous bound (image pull can be slow). ⚠️ killing the exec socket
27
- * does NOT kill the remote process — acceptable here because the pod is DISPOSABLE (destroyed with the task;
28
- * `activeDeadlineSeconds` is the leak guard), unlike the SSH real-machine posture.
29
- *
30
- * Auth: a bearer token + CA, either explicit (`K8S_API_URL`/`K8S_TOKEN`/`K8S_CA_CERT_B64`) or in-cluster
31
- * (service-account files + `KUBERNETES_SERVICE_HOST`) — control-plane config, never agent-reachable.
32
- */
33
1
  import path from "node:path";
34
2
  import { readFileSync } from "node:fs";
35
3
  import https from "node:https";
@@ -44,25 +12,13 @@ import { createPosixShellFs } from "./posix-shell-fs.js";
44
12
  import { randomUUID } from "node:crypto";
45
13
  import { BackgroundShellManager, seedMemStream, feedMemStream, drainMemStream } from "./background-shell-support.js";
46
14
  const PROVIDER = "k8s";
47
- /**
48
- * design/103 background-shell (TOB, path ① — pod-side file buffer re-read by byte cursor). Bounds for kata/k8s.
49
- * A background job cannot outlive its pod (`activeDeadlineSeconds`), so `maxBgTimeoutSec` is additionally clamped
50
- * to the pod lifetime at construction. `maxConcurrent` mirrors the TOC default (8) — anti fork-bomb.
51
- */
52
15
  const K8S_BG_DIR_ROOT = "/tmp/.sema-bg";
53
16
  const K8S_BG_MAX_CONCURRENT = numEnvOr("K8S_BG_MAX_CONCURRENT", 8, 1);
54
17
  const K8S_BG_DEFAULT_TIMEOUT_SEC = numEnvOr("K8S_BG_DEFAULT_TIMEOUT_SEC", 300, 1);
55
18
  const K8S_BG_MAX_TIMEOUT_SEC = numEnvOr("K8S_BG_MAX_TIMEOUT_SEC", 1800, K8S_BG_DEFAULT_TIMEOUT_SEC);
56
- /** Max bytes returned per stream per poll (bounds the control-plane transfer; the model only displays ~20K anyway). */
57
19
  const K8S_BG_READ_CAP = numEnvOr("K8S_BG_READ_CAP", 1024 * 1024, 64 * 1024);
58
- /** Per-stream pod-side file cap (FIFO+`head` — a runaway `yes` is SIGPIPE'd at this bound; the pod has no ephemeral-storage limit). */
59
20
  const K8S_BG_FILE_CAP = numEnvOr("K8S_BG_FILE_CAP", 64 * 1024 * 1024, K8S_BG_READ_CAP);
60
- /** Per-stream control-plane drain cap for an ADOPTED (mid-flight detach) shell — its output rides the still-open
61
- * exec WebSocket into worker memory, not pod files, so bound the un-polled queue (drop-oldest → `truncated`). */
62
21
  const K8S_BG_MEM_CAP = numEnvOr("K8S_BG_MEM_CAP", 8 * 1024 * 1024, 64 * 1024);
63
- // design/158 A13: the pure background-shell pod-side script generation/parsing group (buildBg*Script ×5 +
64
- // buildDetachCapableExec + parseBgPollOutput + ParsedBgPoll) moved VERBATIM to ./k8s-bg-scripts.js —
65
- // value-imported below (the class body still calls them) and re-exported so existing importers keep working.
66
22
  import { buildBgRunnerScript, buildBgLauncherScript, buildBgPollScript, buildBgKillScript, buildBgDisposeScript, buildDetachCapableExec, parseBgPollOutput, } from "./k8s-bg-scripts.js";
67
23
  export { buildBgRunnerScript, buildBgLauncherScript, buildBgPollScript, buildBgKillScript, buildBgDisposeScript, buildDetachCapableExec, parseBgPollOutput, } from "./k8s-bg-scripts.js";
68
24
  const ok = (value) => ({ ok: true, value });
@@ -71,29 +27,12 @@ const unsupported = (op) => ({
71
27
  error: new RemoteExecutionError("unsupported", `${op} is not supported on a Kata pod sandbox (no managed memory snapshot — capabilities.suspendable=false; durable suspend degrades to checkpoint + cold rebuild)`),
72
28
  });
73
29
  const SA_DIR = "/var/run/secrets/kubernetes.io/serviceaccount";
74
- /** Exec WebSocket subprotocols, preference order (see header — v4 burst-stall). */
75
30
  const EXEC_SUBPROTOCOLS = ["v5.channel.k8s.io", "v4.channel.k8s.io"];
76
- /**
77
- * argv-embedded base64 write chunk (SOURCE bytes). 🔴 The bound is Linux `MAX_ARG_STRLEN` = 128KiB (131072),
78
- * the limit on a SINGLE argv string — and the whole `printf '%s' '<base64>' | base64 -d >> '<path>'` command
79
- * is passed as one `sh -c <cmd>` argument. base64 inflates 4/3, so a 96KiB source chunk made a 131072-byte
80
- * base64 = exactly the limit ⇒ silent truncation (night-drill: writes >95KiB failed "command failed"). 64KiB
81
- * source → 87KiB base64 + wrapper ≈ 87KiB command, a safe ~44KiB under the limit.
82
- */
83
31
  const WRITE_CHUNK_BYTES = 64 * 1024;
84
- // design/158 A13: the pod-spec patch group (PodSpecPatch + applyPodSpecPatch + its hardening/volume guards)
85
- // moved VERBATIM to ./k8s-exec-protocol.js — value-imported below (the class body calls applyPodSpecPatch)
86
- // and re-exported so existing importers keep working.
87
32
  import { applyPodSpecPatch } from "./k8s-exec-protocol.js";
88
33
  export { applyPodSpecPatch } from "./k8s-exec-protocol.js";
89
- // design/158 A13: execSocketTlsOptions + exitCodeFromStatus moved VERBATIM to ./k8s-exec-protocol.js
90
- // (errorMessageOf, sandwiched between them in the original layout, stays HERE — it is a small ws-error
91
- // helper, not part of the pod-spec-patch/protocol group). Value-imported below (the class body calls both)
92
- // and re-exported so existing importers keep working.
93
34
  import { execSocketTlsOptions, exitCodeFromStatus } from "./k8s-exec-protocol.js";
94
35
  export { execSocketTlsOptions } from "./k8s-exec-protocol.js";
95
- /** `Error`-or-event → message. Bun's ws shim emits browser-style ErrorEvents (NOT instanceof Error) whose
96
- * String() is "[object ErrorEvent]" — that masked the real TLS failure for a whole drill. */
97
36
  export function errorMessageOf(e) {
98
37
  if (e instanceof Error)
99
38
  return e.message;
@@ -105,15 +44,10 @@ export class RemoteK8sExecutionEnv {
105
44
  capabilities;
106
45
  cwd;
107
46
  podName;
108
- /** Cluster-internal pod address, captured when the pod reaches Running — the LSP `getHost` surface.
109
- * Lifecycle mirrors podName exactly (set on connect, cleared on suspend/destroy/dead-reconnect). */
110
47
  podIP;
111
48
  destroyed = false;
112
- /** Set after a successful resumeVM so workspaceHandle() carries the snapshot it was restored from. */
113
49
  handleSnapshotId;
114
- /** In-flight exec count — `suspendVM` refuses while a command runs (council #4 suspend×in-flight). */
115
50
  inFlightExecs = 0;
116
- /** Memoized in-flight connect (lazy-connect; prevents concurrent double pod-create — the E2B race lesson). */
117
51
  connectPromise;
118
52
  cfg;
119
53
  deps;
@@ -138,31 +72,20 @@ export class RemoteK8sExecutionEnv {
138
72
  ...(config.s3Snapshot ? { s3Snapshot: config.s3Snapshot } : {}),
139
73
  ...(config.sessionId ? { sessionId: config.sessionId } : {}),
140
74
  ...(config.podSpecPatch ? { podSpecPatch: config.podSpecPatch } : {}),
141
- ...(config.podEnv ? { podEnv: config.podEnv } : {}), // RFC B5: region package-source env
75
+ ...(config.podEnv ? { podEnv: config.podEnv } : {}),
142
76
  };
143
77
  this.cwd = this.cfg.mountPath;
144
78
  this.deps = deps ?? {};
145
- // Workspace snapshots make this env suspendable (files durable across suspend/resume); without an S3
146
- // store it stays the honest checkpoint + cold-rebuild degrade.
147
79
  this.capabilities = { isolation: true, suspendable: !!this.cfg.s3Snapshot };
148
- // Clamp BOTH the ceiling AND the default to the pod lifetime — a short-lived pod must not advertise a default
149
- // bigger than its max (the manager would clamp the actual spawn, making the advertised default a lie).
150
80
  const bgMax = Math.max(1, Math.min(K8S_BG_MAX_TIMEOUT_SEC, Math.floor(this.cfg.timeoutMs / 1000)));
151
81
  this.backgroundCapabilities = {
152
82
  supported: true,
153
83
  maxConcurrent: K8S_BG_MAX_CONCURRENT,
154
84
  defaultBgTimeoutSec: Math.min(K8S_BG_DEFAULT_TIMEOUT_SEC, bgMax),
155
85
  maxBgTimeoutSec: bgMax,
156
- // core 1.207 design/116 detach (1.82): a running foreground exec is ADOPTED as a background shell
157
- // (mid-flight ctrl+b) — the exec WebSocket keeps streaming into memory-mode drain buffers; kill works
158
- // through the pgid the detach-capable exec prelude self-reported (buildDetachCapableExec).
159
86
  supportsDetach: true,
160
87
  };
161
88
  }
162
- /** 166-T2 — ExecutionEnv 寿命声明:pod `activeDeadlineSeconds = ceil(timeoutMs/1000)` 是平台真硬死线
163
- * (k8s 到点杀 pod,无 keepalive 续期机制)⇒ 恒声明,core 在死线−60s 处 suspend(基建合格)或响亮
164
- * 终局(env.lifetime_expired)。`lifetimeStartedAt` 有意不声明:core 只在 prepare 期读一次,lazy-connect
165
- * 下 pod 尚未创建 ⇒ core 回落铸造时刻为锚——比 pod 真启动早,提早 suspend 是保守方向。 */
166
89
  get lifetimeMs() {
167
90
  return this.cfg.timeoutMs;
168
91
  }
@@ -172,8 +95,6 @@ export class RemoteK8sExecutionEnv {
172
95
  workspaceHandle() {
173
96
  return { sandboxId: this.podName ?? "(unconnected)", provider: PROVIDER, mountPath: this.cwd, ...(this.handleSnapshotId ? { snapshotId: this.handleSnapshotId } : {}) };
174
97
  }
175
- // ─────────────────────────────── auth / transport ───────────────────────────────
176
- /** Resolved lazily so an in-cluster pod picks up its service account without explicit config. */
177
98
  creds() {
178
99
  const apiUrl = this.cfg.apiUrl ??
179
100
  (process.env.KUBERNETES_SERVICE_HOST ? `https://${process.env.KUBERNETES_SERVICE_HOST}:${process.env.KUBERNETES_SERVICE_PORT ?? "443"}` : undefined);
@@ -199,7 +120,6 @@ export class RemoteK8sExecutionEnv {
199
120
  }
200
121
  return { apiUrl: apiUrl.replace(/\/+$/, ""), token, ca };
201
122
  }
202
- /** Control-plane REST call (pod create/get/delete), bounded by rpcTimeoutMs. Injectable for tests. */
203
123
  async request(method, apiPath, body, opts) {
204
124
  if (this.deps.request)
205
125
  return this.deps.request(method, apiPath, body, opts);
@@ -254,22 +174,15 @@ export class RemoteK8sExecutionEnv {
254
174
  ...(url.startsWith("wss:") ? execSocketTlsOptions(ca, this.cfg.insecureTls) : {}),
255
175
  });
256
176
  }
257
- // ─────────────────────────────── lifecycle ───────────────────────────────
258
177
  async connect(config) {
259
178
  const policy = {
260
- retryableCodes: ["connect_failed", "timeout", "transport_lost"], // NOT auth_failed
179
+ retryableCodes: ["connect_failed", "timeout", "transport_lost"],
261
180
  maxAttempts: this.cfg.retry.maxAttempts,
262
181
  backoffMs: (failed) => this.cfg.retry.backoffMs * failed,
263
182
  };
264
183
  const once = this.deps.connectOnce ?? ((s) => this.connectOnce(s));
265
184
  return withRetry(() => once(config?.abortSignal), policy, { signal: config?.abortSignal });
266
185
  }
267
- /**
268
- * Lazy-connect on first use — 🔴 core NEVER calls `connect()` (verified: the Runner only calls `resumeVM` on
269
- * the resume path), so an adapter that demands an explicit connect can never work through the production
270
- * hand-tool path (lazy-connect belongs in the adapter). Memoized so concurrent first ops share ONE
271
- * pod create; a failed connect clears the memo (next op retries); `destroyed` is terminal.
272
- */
273
186
  async ensureConnected() {
274
187
  if (this.podName)
275
188
  return ok(undefined);
@@ -278,7 +191,7 @@ export class RemoteK8sExecutionEnv {
278
191
  this.connectPromise ??= this.connect();
279
192
  const r = await this.connectPromise;
280
193
  if (!r.ok) {
281
- this.connectPromise = undefined; // don't cache the failure — the next op gets a fresh attempt
194
+ this.connectPromise = undefined;
282
195
  return { ok: false, error: new ExecutionError("shell_unavailable", `k8s sandbox connect failed: ${r.error.message}`, r.error) };
283
196
  }
284
197
  return ok(undefined);
@@ -299,24 +212,16 @@ export class RemoteK8sExecutionEnv {
299
212
  spec: {
300
213
  ...(this.cfg.runtimeClass ? { runtimeClassName: this.cfg.runtimeClass } : {}),
301
214
  restartPolicy: "Never",
302
- // Defense-in-depth (council DESIGN#1): the agent runs INSIDE this pod; never mount a k8s SA token into
303
- // it. The adapter holds the only credential it needs (control-plane side) — the workload gets none, so
304
- // an accidental cluster-wide default-SA binding can't hand the agent the API.
305
215
  automountServiceAccountToken: false,
306
- // Leak guard (the E2B-lifetime analogue): the pod self-terminates even if destroy() never runs.
307
216
  activeDeadlineSeconds: Math.ceil(this.cfg.timeoutMs / 1000),
308
217
  terminationGracePeriodSeconds: 1,
309
218
  containers: [
310
219
  {
311
220
  name: "sandbox",
312
221
  image: this.cfg.image,
313
- // RFC B5: when a pkg-source profile is injected, run the image's materializer ONCE at pod start
314
- // (root, before any exec — core's exec lanes are non-login `sh -c`, so the profile.d hook alone
315
- // wouldn't fire there). Script-absent images no-op; podEnv unset ⇒ command byte-identical to today.
316
222
  command: this.cfg.podEnv?.SEMA_PKG_SOURCE
317
223
  ? ["sh", "-c", "[ -f /opt/sema/pkg-source.sh ] && sh /opt/sema/pkg-source.sh || true; sleep infinity"]
318
224
  : ["sh", "-c", "sleep infinity"],
319
- // RFC B5: operator-trusted region env (pkg-source selection). Absent = no env key at all (byte-identical).
320
225
  ...(this.cfg.podEnv && Object.keys(this.cfg.podEnv).length > 0
321
226
  ? { env: Object.entries(this.cfg.podEnv).map(([name, value]) => ({ name, value: String(value) })) }
322
227
  : {}),
@@ -325,9 +230,6 @@ export class RemoteK8sExecutionEnv {
325
230
  ],
326
231
  },
327
232
  };
328
- // 🔴 ADDITIVE deployment patch: add RO oracle mounts / hardened securityContext for the L8
329
- // immutable-mount probe + the graderEnvFactory. Applied AFTER the hard invariants above so it can never weaken
330
- // them (applyPodSpecPatch only touches volumes/mounts/securityContext/labels). No-op when unset (byte-identical).
331
233
  if (this.cfg.podSpecPatch)
332
234
  applyPodSpecPatch(pod, this.cfg.podSpecPatch);
333
235
  let created;
@@ -346,7 +248,6 @@ export class RemoteK8sExecutionEnv {
346
248
  const name = created.json?.metadata?.name;
347
249
  if (!name)
348
250
  return { ok: false, error: new RemoteExecutionError("connect_failed", "pod create returned no name") };
349
- // Poll until Running (schedule + image pull + Kata VM boot), bounded by readyTimeoutMs.
350
251
  const deadline = Date.now() + this.cfg.readyTimeoutMs;
351
252
  let podIP;
352
253
  for (;;) {
@@ -383,16 +284,12 @@ export class RemoteK8sExecutionEnv {
383
284
  }
384
285
  await new Promise((r) => setTimeout(r, 1000));
385
286
  }
386
- // Destroy-race guard (the E2B lazy-connect BLOCKER lesson): if destroy() ran while we were creating the
387
- // pod, kill the fresh pod and bail — never resurrect a destroyed env (destroyed is terminal).
388
287
  if (this.destroyed) {
389
288
  await this.deletePod(name).catch(() => { });
390
289
  return { ok: false, error: new RemoteExecutionError("connect_failed", "env destroyed during connect") };
391
290
  }
392
291
  this.podName = name;
393
292
  this.podIP = podIP;
394
- // Workspace root (the image may not ship it) — via runExec DIRECTLY: this.exec() would re-enter
395
- // ensureConnected and deadlock on the still-pending connect memo. Failure = unusable sandbox.
396
293
  const mk = await this.runExec(`mkdir -p ${shellQuote(this.cfg.mountPath)}`);
397
294
  if (!mk.ok || mk.value.exitCode !== 0) {
398
295
  await this.deletePod(name).catch(() => { });
@@ -402,14 +299,6 @@ export class RemoteK8sExecutionEnv {
402
299
  }
403
300
  return ok(this.workspaceHandle());
404
301
  }
405
- /**
406
- * Build a validated, presigned URL for a snapshot object — `<prefix>/<sessionId>/<snapshotId>.tar.gz`.
407
- * 🔴 sessionId comes from the client (`body.sessionId`, security.ts) and is NOT charset-validated upstream, so
408
- * each path segment is allowlisted here (`^[A-Za-z0-9][A-Za-z0-9._-]*$`) before it enters the object key: a
409
- * segment of `..` / `../x` / one containing `/` would otherwise let one tenant's snapshot key normalize into
410
- * another tenant's prefix (cross-tenant read on resume / clobber on suspend). Returns a typed error instead of
411
- * signing a traversing key. Folds the old snapshotKey()+s3Presign() pair so the two call sites can't diverge.
412
- */
413
302
  presignSnapshot(method, snapshotId) {
414
303
  const s3 = this.cfg.s3Snapshot;
415
304
  const prefix = s3.keyPrefix ?? "kata";
@@ -429,48 +318,29 @@ export class RemoteK8sExecutionEnv {
429
318
  accessKey: s3.accessKey,
430
319
  secretKey: s3.secretKey,
431
320
  ...(s3.region ? { region: s3.region } : {}),
432
- // Clamp the presigned-URL TTL to a 1h ceiling — a short-lived single-object credential must not become a
433
- // long-lived bearer token via an over-large WORKSPACE_SNAPSHOT_TTL_SEC.
434
321
  expiresSec: Math.min(s3.presignTtlSec ?? 600, 3600),
435
322
  }),
436
323
  };
437
324
  }
438
- /** Redact a presigned URL (and any lingering signature) from text before it enters an error/log — the URL is a
439
- * short-lived bearer credential and must never reach a durable store (secret boundary). */
440
325
  redactUrl(text, url) {
441
326
  return text.split(url).join("<presigned-url>").replace(/X-Amz-Signature=[0-9a-f]+/gi, "X-Amz-Signature=<redacted>");
442
327
  }
443
- /**
444
- * Workspace-durable suspend: tar the workspace, upload to S3 via a short-lived adapter-presigned
445
- * URL the pod curls (credentials never enter the sandbox), then delete the pod. The returned SnapshotId is
446
- * opaque to core; we use a fresh id and persist the bytes under {@link snapshotKey}. NOT supported without an
447
- * S3 store (returns the honest "unsupported" so durable suspend degrades to checkpoint + cold rebuild).
448
- *
449
- * 🔴 council #4 (suspend × in-flight exec): refuse while a command runs — tarring a mutating tree would
450
- * snapshot a torn workspace. Core also won't suspend mid-tool, but the adapter enforces it independently.
451
- */
452
328
  async suspendVM(_options) {
453
329
  if (!this.cfg.s3Snapshot)
454
330
  return unsupported("suspendVM");
455
331
  if (this.inFlightExecs > 0)
456
332
  return { ok: false, error: new RemoteExecutionError("command_in_flight", "cannot suspend while a command is in flight (would snapshot a torn workspace)") };
457
333
  const snapshotId = `kata-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
458
- // A never-connected env (a plan-only task that suspended without touching the hand) has no workspace to
459
- // capture — return a snapshotId with no object; resume tolerates the 404 as an empty workspace.
460
334
  if (!this.podName)
461
335
  return ok(snapshotId);
462
336
  const presigned = this.presignSnapshot("PUT", snapshotId);
463
337
  if (!presigned.ok)
464
338
  return presigned;
465
339
  const url = presigned.value;
466
- // Materialize the archive to a pod-local temp file, THEN upload — a streamed `tar | curl -T -` sends
467
- // Transfer-Encoding: chunked with no Content-Length, which MinIO rejects with HTTP 411 on a presigned PUT
468
- // (live-verified). `curl -T <file>` sets Content-Length from the file size. Bytes still go pod→MinIO (never
469
- // the worker); the temp file lives in the disposable pod and is removed regardless of curl's outcome.
470
340
  const tmp = "/tmp/ws-snap.tgz";
471
341
  const cmd = `tar czf ${tmp} -C ${shellQuote(this.cfg.mountPath)} . && curl -fsS -X PUT -T ${tmp} ${shellQuote(url)}; ` +
472
342
  `rc=$?; rm -f ${tmp}; exit $rc`;
473
- const r = await this.runExec(cmd, { timeoutMs: this.cfg.dataTimeoutMs, livenessMs: 0 }); // silent bulk upload → wall bound only
343
+ const r = await this.runExec(cmd, { timeoutMs: this.cfg.dataTimeoutMs, livenessMs: 0 });
474
344
  if (!r.ok)
475
345
  return { ok: false, error: new RemoteExecutionError("connect_failed", `workspace snapshot upload failed: ${this.redactUrl(r.error.message, url)}`, r.error) };
476
346
  if (r.value.exitCode !== 0)
@@ -478,24 +348,12 @@ export class RemoteK8sExecutionEnv {
478
348
  const name = this.podName;
479
349
  this.podName = undefined;
480
350
  this.podIP = undefined;
481
- await this.deletePod(name).catch(() => { }); // suspend = release the pod; the snapshot is the durable state
351
+ await this.deletePod(name).catch(() => { });
482
352
  return ok(snapshotId);
483
353
  }
484
- /**
485
- * Resume a workspace snapshot onto a FRESH pod (resume = executionEnvFactory rebuild + resumeVM):
486
- * provision a pod, then download + untar the snapshot into the workspace. A missing object (empty/plan-only
487
- * snapshot) resumes as a clean workspace.
488
- *
489
- * [1467]①(priorHandle 根保真)——k8s 走**诚实臂**而非绑定臂:pod 的 workspace volume 挂在**当前**
490
- * 配置根(cfg.mountPath),往 priorHandle.mountPath 绑=往容器 rootfs untar 大工作区,ephemeral-storage
491
- * 触顶即 pod 驱逐(恢复中途死,比「根变了 fail-closed reopen」更糟)。所以 untar 落当前根、handle 回报
492
- * 真生效根(core 契约明许:「做不到就诚实返回实际 mountPath,勿回显未兑现值」);跨 K8S_MOUNT_PATH
493
- * 变更窗的分歧由 core 侧 fail-closed reopen 兜底,审批不烧错根。
494
- */
495
354
  async resumeVM(snapshotId, options) {
496
355
  if (!this.cfg.s3Snapshot)
497
356
  return unsupported("resumeVM");
498
- // Validate + presign BEFORE provisioning a pod — a traversing/garbage snapshotId should fail fast, not leak a pod.
499
357
  const presigned = this.presignSnapshot("GET", snapshotId);
500
358
  if (!presigned.ok)
501
359
  return presigned;
@@ -503,14 +361,11 @@ export class RemoteK8sExecutionEnv {
503
361
  if (!c.ok)
504
362
  return c;
505
363
  const url = presigned.value;
506
- // Capture the HTTP status (curl -s -w) and branch on it: 404 → empty/plan-only snapshot, leave the fresh
507
- // empty workspace; non-200 → real error (exit 22); 200 → untar. (No `-f`: we need the status code, not a
508
- // curl error exit, to distinguish the tolerated 404 from a genuine failure.)
509
364
  const cmd = `code=$(curl -s -o /tmp/ws.tgz -w '%{http_code}' ${shellQuote(url)}); ` +
510
365
  `if [ "$code" = "404" ]; then echo EMPTY_SNAPSHOT; exit 0; fi; ` +
511
366
  `if [ "$code" != "200" ]; then echo "snapshot GET HTTP $code" >&2; exit 22; fi; ` +
512
367
  `tar xzf /tmp/ws.tgz -C ${shellQuote(this.cfg.mountPath)} && rm -f /tmp/ws.tgz`;
513
- const r = await this.runExec(cmd, { timeoutMs: this.cfg.dataTimeoutMs, livenessMs: 0 }); // silent bulk download → wall bound only
368
+ const r = await this.runExec(cmd, { timeoutMs: this.cfg.dataTimeoutMs, livenessMs: 0 });
514
369
  if (!r.ok)
515
370
  return { ok: false, error: new RemoteExecutionError("connect_failed", `workspace snapshot restore failed: ${this.redactUrl(r.error.message, url)}`, r.error) };
516
371
  if (r.value.exitCode !== 0)
@@ -518,19 +373,12 @@ export class RemoteK8sExecutionEnv {
518
373
  this.handleSnapshotId = snapshotId;
519
374
  return ok(this.workspaceHandle());
520
375
  }
521
- /**
522
- * Honest post-resume consistency (design/48 §5 council #8): the workspace FILES are restored byte-for-byte
523
- * from the snapshot, so read-before-edit hashing already lines up — there's nothing to re-fetch. In-VM
524
- * process state (a language server) is gone by design; core/the LSP manager re-establishes it lazily. So
525
- * this is a successful no-op rather than the previous `unsupported` (which forced a cold rebuild).
526
- */
527
376
  async postResumeInit() {
528
377
  if (!this.cfg.s3Snapshot)
529
378
  return unsupported("postResumeInit");
530
379
  return ok(undefined);
531
380
  }
532
381
  async reconnect(_sessionToken) {
533
- // The pod either still runs (reuse) or is gone (fresh connect).
534
382
  if (this.podName) {
535
383
  const alive = await this.request("GET", `/api/v1/namespaces/${this.cfg.namespace}/pods/${this.podName}`).catch(() => undefined);
536
384
  const phase = alive?.json?.status?.phase;
@@ -544,10 +392,7 @@ export class RemoteK8sExecutionEnv {
544
392
  async deletePod(name) {
545
393
  await this.request("DELETE", `/api/v1/namespaces/${this.cfg.namespace}/pods/${name}?gracePeriodSeconds=0`, undefined, { timeoutMs: this.cfg.rpcTimeoutMs });
546
394
  }
547
- /** Delete the sandbox pod — the workspace is DISPOSABLE (vs SSH/ADB disconnect-only). Idempotent, never throws. */
548
395
  async destroy() {
549
- // design/103 §3.7 backstop: kill/clear any background jobs while the pod is still alive (core's run-loop finally
550
- // usually disposed already → this iterates an empty registry). After this the pod delete reaps anything left.
551
396
  await this.disposeBackgroundShells().catch(() => { });
552
397
  this.destroyed = true;
553
398
  const name = this.podName;
@@ -559,7 +404,6 @@ export class RemoteK8sExecutionEnv {
559
404
  async cleanup() {
560
405
  return this.destroy();
561
406
  }
562
- // ─────────────────────────────── exec ───────────────────────────────
563
407
  execUrl(command) {
564
408
  const { apiUrl, token } = this.creds();
565
409
  const base = apiUrl.replace(/^http/, "ws");
@@ -571,18 +415,7 @@ export class RemoteK8sExecutionEnv {
571
415
  q.append("command", part);
572
416
  return { url: `${base}/api/v1/namespaces/${this.cfg.namespace}/pods/${this.podName}/exec?${q.toString()}`, headers: { authorization: `Bearer ${token}` } };
573
417
  }
574
- /**
575
- * Run a command over the exec WebSocket. Frames: byte0=channel (1 stdout / 2 stderr / 3 status JSON with the
576
- * REAL exit code). Bounds: caller wall-clock (explicit `timeout`) + zero-progress liveness (`livenessMs`) —
577
- * a socket killed by either does NOT kill the remote process (disposable-pod posture, see header).
578
- */
579
- runExec(command,
580
- // 🔴 binary caveat (D2): runExec is SHARED by the model-facing TEXT exec / execStream (boundOutput:true —
581
- // a `yes`/`cat big` must not OOM the durable worker) AND by readBinaryFile, which reads the file as base64 over
582
- // execData→runExec and decodes r.value.stdout. Bounding that base64 stream would SILENTLY CORRUPT binary/large
583
- // reads. So boundOutput DEFAULTS OFF: every data path (execData, snapshot tar up/down, mkdir) keeps EVERY byte
584
- // byte-exact; ONLY exec()/execStream() opt in. stderr is always bounded (never the file payload).
585
- opts) {
418
+ runExec(command, opts) {
586
419
  let sock;
587
420
  let headers;
588
421
  let url;
@@ -594,27 +427,19 @@ export class RemoteK8sExecutionEnv {
594
427
  return Promise.resolve({ ok: false, error: new ExecutionError("shell_unavailable", e instanceof Error ? e.message : String(e), e instanceof Error ? e : undefined) });
595
428
  }
596
429
  return new Promise((resolvePromise) => {
597
- // boundOutput (see signature): TEXT exec/execStream bound stdout to a rolling-TAIL (last 8MB + dropped marker,
598
- // O(maxBytes)); data paths (boundOutput off) keep full bytes so readBinaryFile's base64 decodes losslessly.
599
- // stderr is ALWAYS bounded — it never carries the base64 file payload.
600
430
  const boundOutput = !!opts?.boundOutput;
601
- const outBufs = []; // data path (boundOutput off): full bytes
602
- const outTail = boundOutput ? new RollingTailBuffer() : undefined; // text path: bounded stdout
431
+ const outBufs = [];
432
+ const outTail = boundOutput ? new RollingTailBuffer() : undefined;
603
433
  const errBuf = new RollingTailBuffer();
604
- // Per-channel UTF-8 decoders for the STREAMING callbacks only (council BUG#1): a multibyte codepoint can
605
- // straddle two WS frames → a per-frame `payload.toString("utf8")` emits U+FFFD. StringDecoder buffers the
606
- // incomplete tail across writes. (The buffered path decodes once via Buffer.concat below — already safe.)
607
434
  const outDecoder = opts?.onStdout ? new StringDecoder("utf8") : undefined;
608
435
  const errDecoder = opts?.onStderr ? new StringDecoder("utf8") : undefined;
609
436
  let statusFrame;
610
437
  let settled = false;
611
438
  let lastProgress = Date.now();
612
- // design/116 detach: once set, the socket's frames belong to the ADOPTED shell — message/error/close route
613
- // here instead of the (already-settled) foreground buffers, and the socket is NOT closed on settle.
614
439
  let spliceSink;
615
- let sinkClosed = false; // error+close can BOTH fire — notify the adopted shell's terminal exactly once
440
+ let sinkClosed = false;
616
441
  let removeDetachListener;
617
- const detachTee = opts?.detach?.tee; // fed on every fg frame; a later adoption takes the pair over as-is (review #3)
442
+ const detachTee = opts?.detach?.tee;
618
443
  const finish = (r) => {
619
444
  if (settled)
620
445
  return;
@@ -627,19 +452,12 @@ export class RemoteK8sExecutionEnv {
627
452
  (sock.terminate ?? sock.close).call(sock);
628
453
  }
629
454
  catch {
630
- /* already gone */
631
455
  }
632
456
  resolvePromise(r);
633
457
  };
634
458
  const wallMs = opts?.timeoutMs;
635
459
  const wallTimer = wallMs ? setTimeout(() => finish({ ok: false, error: new ExecutionError("timeout", `command exceeded ${wallMs}ms`) }), wallMs) : undefined;
636
- // zero-progress liveness (E2B class-fix semantics): on firing, re-check actual idleness and re-arm for
637
- // EXACTLY the remaining window (the E2B waitWithLiveness/council-DESIGN-1 discipline — a full-window
638
- // re-arm would let a hang sit for up to 2× livenessMs before detection).
639
460
  let livenessTimer;
640
- // Per-call liveness override (0 = disabled): bulk data transfers (tar→S3 upload, S3→untar restore) are
641
- // legitimately SILENT for their whole duration, so the zero-output liveness would false-positive a "hang";
642
- // they rely on the wall-clock `timeoutMs` (dataTimeoutMs) instead. Defaults to the configured command bound.
643
461
  const livenessMs = opts?.livenessMs ?? this.cfg.livenessMs;
644
462
  const armLiveness = () => {
645
463
  if (livenessMs <= 0)
@@ -666,10 +484,8 @@ export class RemoteK8sExecutionEnv {
666
484
  return;
667
485
  lastProgress = Date.now();
668
486
  const channel = buf[0];
669
- const payload = buf.subarray(1); // zero-copy view; `ws` gives each message its own buffer (no reuse),
487
+ const payload = buf.subarray(1);
670
488
  if (spliceSink) {
671
- // Adopted: output frames feed the background shell's drain buffers; the status frame still lands in
672
- // `statusFrame` so the close handler can classify the adopted shell's terminal.
673
489
  if (channel === 1 || channel === 2)
674
490
  spliceSink.data(channel, payload);
675
491
  else if (channel === 3)
@@ -678,11 +494,11 @@ export class RemoteK8sExecutionEnv {
678
494
  }
679
495
  if (channel === 1) {
680
496
  if (boundOutput)
681
- outTail.push(payload); // text path: bounded tail (streaming callback below still gets the full chunk)
497
+ outTail.push(payload);
682
498
  else
683
- outBufs.push(payload); // data path: full bytes, safe to retain until the final Buffer.concat (council perf#4)
499
+ outBufs.push(payload);
684
500
  if (detachTee)
685
- feedMemStream(detachTee.out, payload, K8S_BG_MEM_CAP); // tee-from-start (review #3): one decoder spans the detach instant
501
+ feedMemStream(detachTee.out, payload, K8S_BG_MEM_CAP);
686
502
  if (outDecoder)
687
503
  opts.onStdout(outDecoder.write(payload));
688
504
  }
@@ -700,8 +516,6 @@ export class RemoteK8sExecutionEnv {
700
516
  sock.on("error", (...args) => {
701
517
  const e = args[0];
702
518
  if (spliceSink) {
703
- // Adopted: a socket error mid-background = the output feed is lost; outcome unknown → failed. The
704
- // process itself may still run in the pod (disposable-pod posture — teardown reaps it).
705
519
  if (!sinkClosed) {
706
520
  sinkClosed = true;
707
521
  spliceSink.closed({ failed: true });
@@ -715,16 +529,12 @@ export class RemoteK8sExecutionEnv {
715
529
  if (sinkClosed)
716
530
  return;
717
531
  sinkClosed = true;
718
- // Terminal classification mirrors the launch-mode poll: a status frame with an exit code ⇒ exited
719
- // (non-zero exit is a NORMAL result); no status ⇒ the transport died mid-background ⇒ failed.
720
532
  if (statusFrame === undefined)
721
533
  return spliceSink.closed({ failed: true });
722
534
  const parsed = exitCodeFromStatus(statusFrame);
723
535
  return spliceSink.closed("error" in parsed ? { failed: true } : { failed: false, exitCode: parsed.exitCode });
724
536
  }
725
537
  if (statusFrame === undefined) {
726
- // closed without a status frame = the transport died mid-command, NOT a command result ([R78]#1).
727
- // Typed "transport_lost" since core 1.89 (outcome UNKNOWN — retry only per the op's idempotency).
728
538
  finish({ ok: false, error: new ExecutionError("transport_lost", "exec socket closed without status — k8s transport lost mid-command") });
729
539
  return;
730
540
  }
@@ -740,32 +550,21 @@ export class RemoteK8sExecutionEnv {
740
550
  finish(ok({ stdout: markTruncated(out.text, out.droppedBytes), stderr, exitCode: parsed.exitCode }));
741
551
  }
742
552
  else {
743
- // byte-exact: readBinaryFile decodes this stdout as base64; never truncate.
744
553
  finish(ok({ stdout: Buffer.concat(outBufs).toString("utf8"), stderr, exitCode: parsed.exitCode }));
745
554
  }
746
555
  });
747
- // design/116 detach — wired AFTER every handler above exists (the host adapter's create-then-abort TDZ
748
- // lesson): core's ToolDetachHub can hand an ALREADY-aborted signal (detach requested before the exec
749
- // started), in which case onDetach runs synchronously right here.
750
556
  const detach = opts?.detach;
751
557
  if (detach) {
752
558
  const onDetach = () => {
753
559
  if (settled || spliceSink)
754
560
  return;
755
- const out = outTail ? outTail.result() : { text: "", droppedBytes: 0 }; // detach rides the exec path (boundOutput:true)
561
+ const out = outTail ? outTail.result() : { text: "", droppedBytes: 0 };
756
562
  const err = errBuf.result();
757
563
  const shellId = detach.adopt({ sock, reroute: (sink) => { spliceSink = sink; } });
758
564
  if (shellId === undefined) {
759
- // Refused (limit / builder failure) → the foreground continues untouched. 🔴 review #5: `reroute` is a
760
- // builder SIDE EFFECT — if the builder ran it and then threw, adoptSync's catch returns undefined but
761
- // spliceSink is already set, which would silently reroute every subsequent fg frame into an unregistered
762
- // buffer AND make this exec never settle (close would notify a sink nobody owns). Un-splice defensively;
763
- // same-tick, so no frame can have landed in between.
764
565
  spliceSink = undefined;
765
566
  return;
766
567
  }
767
- // Settle EARLY *without* closing the socket — the adopted shell owns it now. finish() is deliberately
768
- // NOT used (it terminates the socket); this is its detach twin.
769
568
  settled = true;
770
569
  clearTimeout(wallTimer);
771
570
  clearTimeout(livenessTimer);
@@ -786,29 +585,21 @@ export class RemoteK8sExecutionEnv {
786
585
  const c = await this.ensureConnected();
787
586
  if (!c.ok)
788
587
  return c;
789
- // design/116 detach (core 1.207, 1.82): the Bash tool threads a `detachSignal` for every exec once
790
- // `supportsDetach` is advertised. `ExecOpts` doesn't type it yet (core-side seam) — read it structurally.
791
588
  const detachSignal = options?.detachSignal;
792
- this.inFlightExecs++; // suspendVM refuses while > 0 (council #4: never snapshot a running command)
589
+ this.inFlightExecs++;
793
590
  try {
794
591
  const composed = this.withCwdEnv(command, options?.cwd, options?.env);
795
592
  if (!detachSignal) {
796
593
  return await this.runExec(composed, {
797
594
  signal: options?.abortSignal,
798
595
  timeoutMs: options?.timeout != null ? options.timeout * 1000 : undefined,
799
- boundOutput: true, // model-facing TEXT exec — bound the buffered return value (D2)
596
+ boundOutput: true,
800
597
  onStdout: options?.onStdout,
801
598
  onStderr: options?.onStderr,
802
599
  });
803
600
  }
804
- // Detach-capable exec: the pgid prelude (buildDetachCapableExec) makes the running command adoptable —
805
- // kill/dispose on the adopted shell reuse the SAME pod-side scripts as a spawned one. On detach, adoptSync
806
- // splices the still-open exec socket into memory-mode drain buffers (SYNCHRONOUS — no await between the
807
- // tee handover and the reroute, the host adapter's double-review contract).
808
601
  const adoptId = randomUUID();
809
602
  const dir = `${K8S_BG_DIR_ROOT}/exec-${adoptId}`;
810
- // tee-from-start (Fable-5 review #3): runExec feeds every fg frame into this pair through its per-stream
811
- // StringDecoder; adoption takes the pair over as-is, so ONE decoder spans the detach instant (no U+FFFD seam).
812
603
  const tee = { out: seedMemStream(""), err: seedMemStream("") };
813
604
  return await this.runExec(buildDetachCapableExec(dir, composed), {
814
605
  signal: options?.abortSignal,
@@ -820,11 +611,8 @@ export class RemoteK8sExecutionEnv {
820
611
  signal: detachSignal,
821
612
  tee,
822
613
  adopt: (io) => {
823
- // 🔴 builder contract (review #5): everything inside is NON-THROWING (object construction + the reroute
824
- // assignment). adoptSync's catch does NOT undo side effects — future logic that can throw must go before
825
- // `io.reroute` (the runExec caller also un-splices defensively on an undefined return).
826
614
  const adopted = this.bgManager.adoptSync((ctx) => {
827
- const mem = { out: tee.out, err: tee.err }; // the tee already holds the full fg history (truncation marker rides on the exec's OWN early result only — review LOW)
615
+ const mem = { out: tee.out, err: tee.err };
828
616
  io.reroute({
829
617
  data: (channel, payload) => feedMemStream(channel === 1 ? mem.out : mem.err, payload, K8S_BG_MEM_CAP),
830
618
  closed: (terminal) => ctx.onTerminal(terminal.failed, terminal.exitCode),
@@ -840,28 +628,13 @@ export class RemoteK8sExecutionEnv {
840
628
  this.inFlightExecs--;
841
629
  }
842
630
  }
843
- /**
844
- * LSP surface (lsp/manager.ts `isLspCapable` duck-type, design/64 §13.1): cluster-internal address for a
845
- * port on the sandbox pod. The E2B counterpart returns a public proxy host; a pod has none — the WORKER
846
- * must be able to reach pod IPs (in-cluster deployment, or on-node), and the transport is plain `ws://`
847
- * (cluster network, no TLS — the per-session bridge token still gates access like on E2B).
848
- */
849
631
  async getHost(port) {
850
632
  const c = await this.ensureConnected();
851
633
  if (!c.ok || !this.podIP)
852
- return undefined; // no IP captured (e.g. faked/older apiserver) → LSP degrades
634
+ return undefined;
853
635
  return `${this.podIP}:${port}`;
854
636
  }
855
- /**
856
- * Start a long-lived helper (the LSP bridge) detached from this exec session. Unlike E2B — which reaps a
857
- * foreground command's process group on completion, forcing a native background API — k8s exec just closes
858
- * the session: a `nohup`'d child with detached stdio survives, reparented to the container's PID 1 (the
859
- * gate#2 background-gradle runs proved this through the production path). Output goes to /tmp for postmortem.
860
- */
861
637
  async startBackground(command) {
862
- // `command` is a COMPOUND shell line (lspStartCommand: `cd … && ENV=… node bridge.cjs`) — nohup takes a
863
- // single executable, so wrap the whole line in `sh -c`. Caught live: `nohup cd …` silently started nothing
864
- // and the bridge connect timed out (gate#2 k8s-LSP attempt 1).
865
638
  const wrapped = command.replace(/[\\"$`]/g, (c) => "\\" + c);
866
639
  const r = await this.exec(`(nohup sh -c "${wrapped}" >>/tmp/lsp-bridge.log 2>&1 &) ; echo started`, { timeout: 30 });
867
640
  if (!r.ok)
@@ -873,7 +646,7 @@ export class RemoteK8sExecutionEnv {
873
646
  const c = await this.ensureConnected();
874
647
  if (!c.ok)
875
648
  throw new RemoteExecutionError("connect_failed", c.error.message, c.error);
876
- this.inFlightExecs++; // suspendVM refuses while > 0 (council #4); decremented in the finally below
649
+ this.inFlightExecs++;
877
650
  const queue = [];
878
651
  let finished = false;
879
652
  let failure;
@@ -886,21 +659,12 @@ export class RemoteK8sExecutionEnv {
886
659
  };
887
660
  let lastChunk = Date.now();
888
661
  const idleMs = options?.readTimeoutMs ?? this.cfg.livenessMs;
889
- // Total-output cap (D2): parity with the host/local-docker execStream — when the caller sets
890
- // maxOutputBytes, count yielded bytes and abort+throw past the cap (the finally's ac.abort() then closes
891
- // the exec socket). Previously k8s execStream IGNORED maxOutputBytes (it was the only adapter that did).
892
662
  const cap = options?.maxOutputBytes;
893
663
  let bytes = 0;
894
- // Internal abort composed with the caller's: EVERY generator exit (normal end, idle-throw, consumer
895
- // early-break via .return()) must settle runExec IMMEDIATELY — without this, the finally's `await done`
896
- // would sit until runExec's own liveness fired (~minutes) with the exec socket held open (self-review A).
897
664
  const ac = new AbortController();
898
665
  const signal = options?.signal ? AbortSignal.any([options.signal, ac.signal]) : ac.signal;
899
666
  const done = this.runExec(this.withCwdEnv(command, options?.cwd, options?.env), {
900
667
  signal,
901
- // The consumer reads from the onStdout/onStderr callbacks (full chunks); runExec's retained buffer is
902
- // discarded here (only exitCode is used). boundOutput:true caps that otherwise-unbounded retained buffer
903
- // with zero change to what the stream consumer sees (D2).
904
668
  boundOutput: true,
905
669
  onStdout: (d) => {
906
670
  lastChunk = Date.now();
@@ -916,12 +680,9 @@ export class RemoteK8sExecutionEnv {
916
680
  if (r.ok)
917
681
  exitCode = r.value.exitCode;
918
682
  else {
919
- // Map the buffered-exec ExecutionErrorCode → a RemoteExecutionErrorCode. `spawn_error`/`shell_unavailable`
920
- // mean the environment is fundamentally broken → "unknown" (non-retryable), NOT "connect_failed" (which
921
- // the retry whitelist would re-attempt against a broken shell — council BUG#2).
922
683
  const code = r.error.code === "aborted"
923
684
  ? "aborted"
924
- : r.error.code === "transport_lost" // typed end-to-end since core 1.89 (was a message marker)
685
+ : r.error.code === "transport_lost"
925
686
  ? "transport_lost"
926
687
  : r.error.code === "timeout"
927
688
  ? "timeout"
@@ -949,10 +710,6 @@ export class RemoteK8sExecutionEnv {
949
710
  if (finished)
950
711
  break;
951
712
  const waitMs = Math.max(1, idleMs - (Date.now() - lastChunk));
952
- // A-002.11: the wait races the drain timer against `wake` (a chunk / completion). When `wake` wins —
953
- // the common case on a chatty command — the timer stays armed for up to 30s unless it is cleared, so a
954
- // long-running command leaks one per drain cycle. Same `finally` cleanup form the E2B twin already uses
955
- // for its idle timer (remote-env-e2b.ts execStream). Clearing an already-fired timer is a no-op.
956
713
  let waitTimer;
957
714
  try {
958
715
  await new Promise((r) => {
@@ -973,23 +730,11 @@ export class RemoteK8sExecutionEnv {
973
730
  yield { type: "exit", exitCode };
974
731
  }
975
732
  finally {
976
- ac.abort(); // settle runExec now (no-op if already finished) — closes the socket, frees the timers
733
+ ac.abort();
977
734
  await done.catch(() => { });
978
735
  this.inFlightExecs--;
979
736
  }
980
737
  }
981
- // ─────────────────────────── design/103 background shell (TOB, path ①) ───────────────────────────
982
- //
983
- // kata/k8s takes path ① (design/103 §5.2): the pod filesystem buffers output, re-read by byte cursor each poll
984
- // — unlike E2B there is no provider job handle, and closing the exec socket does NOT kill the remote process
985
- // (disposable-pod posture, see file header), so termination is detected by an exit-sentinel file and kill goes
986
- // through the process GROUP (`setsid` + self-reported pgid). Background ops run as short ephemeral execs and
987
- // deliberately do NOT touch `inFlightExecs` (they are control-plane management, not a foreground command to
988
- // snapshot; the detached job is killed before `suspendVM` by the core dispose saga, design/103 §3.7).
989
- /**
990
- * A background job cannot outlive its pod (`activeDeadlineSeconds` ≈ `timeoutMs`), so the BG ceiling is clamped to
991
- * it (assigned in the constructor — `this.cfg` is set there, so a field initializer would read it pre-init).
992
- */
993
738
  backgroundCapabilities;
994
739
  _bgManager;
995
740
  get bgManager() {
@@ -1002,11 +747,9 @@ export class RemoteK8sExecutionEnv {
1002
747
  async launch(command, options, ctx) {
1003
748
  const conn = await self.ensureConnected();
1004
749
  if (!conn.ok)
1005
- throw new Error(`pod connect failed: ${conn.error.message}`); // → manager wraps as spawn_failed
750
+ throw new Error(`pod connect failed: ${conn.error.message}`);
1006
751
  const jobId = randomUUID();
1007
752
  const dir = `${K8S_BG_DIR_ROOT}/${jobId}`;
1008
- // 🔴 secret-scrub red line (design/103 §3.1): cwd + env injected via the SAME `withCwdEnv` path as exec —
1009
- // the host process.env is never injected; only the scrubbed task env core passes reaches the child.
1010
753
  const inner = self.withCwdEnv(command, options?.cwd, options?.env);
1011
754
  const cmdB64 = Buffer.from(inner, "utf8").toString("base64");
1012
755
  const runnerB64 = Buffer.from(buildBgRunnerScript(dir), "utf8").toString("base64");
@@ -1016,12 +759,10 @@ export class RemoteK8sExecutionEnv {
1016
759
  if (r.value.exitCode !== 0 || !r.value.stdout.includes("SEMABG_LAUNCHED")) {
1017
760
  throw new Error(`background launch exited ${r.value.exitCode}: ${r.value.stderr.slice(0, 200)}`);
1018
761
  }
1019
- void ctx; // k8s is pull-model: termination is discovered in read(); ctx.onTerminal is intentionally unused
762
+ void ctx;
1020
763
  return { jobId, dir, stdoutCursor: 0, stderrCursor: 0 };
1021
764
  },
1022
765
  async read(state) {
1023
- // ADOPT (memory) mode: the still-open exec socket feeds the drain buffers; termination is PUSH-model
1024
- // (socket close → ctx.onTerminal), so `read` never reports terminal here — mirror of the E2B/host adopt.
1025
766
  if (state.adopt) {
1026
767
  const o = drainMemStream(state.adopt.mem.out);
1027
768
  const e = drainMemStream(state.adopt.mem.err);
@@ -1039,20 +780,15 @@ export class RemoteK8sExecutionEnv {
1039
780
  if (!p)
1040
781
  throw new Error("background poll: unparseable response");
1041
782
  if (p.gone) {
1042
- // dir vanished while still owned (dispose clears the registry FIRST, so this isn't us) → abnormal end.
1043
783
  return { stdout: "", stderr: "", bytesFromStart: state.stdoutCursor + state.stderrCursor, bytesDroppedBeforeCursor: 0, terminal: { failed: true } };
1044
784
  }
1045
785
  const droppedO = Math.max(0, p.outTotal - state.stdoutCursor - p.stdoutBytes);
1046
786
  const droppedE = Math.max(0, p.errTotal - state.stderrCursor - p.stderrBytes);
1047
787
  state.stdoutCursor = p.outTotal;
1048
788
  state.stderrCursor = p.errTotal;
1049
- // Terminal: exit sentinel present → exited (exit code known); else process group gone without a sentinel →
1050
- // failed (abnormal — a clean exit always writes the sentinel as the runner's last act before the group dies).
1051
789
  let terminal = p.exitCode !== undefined ? { failed: false, exitCode: p.exitCode } : !p.alive ? { failed: true } : undefined;
1052
- // 🔴 disk guard: direct redirect has no in-pod cap, so reap a runaway whose output exceeds K8S_BG_FILE_CAP
1053
- // (the kata VM isolates the blast to this disposable pod; the BG-timeout + activeDeadlineSeconds also bound it).
1054
790
  if (!terminal && (p.outTotal > K8S_BG_FILE_CAP || p.errTotal > K8S_BG_FILE_CAP)) {
1055
- void bgExec(buildBgKillScript(state.dir), true).catch(() => { }); // fire-and-forget reap
791
+ void bgExec(buildBgKillScript(state.dir), true).catch(() => { });
1056
792
  terminal = { failed: true };
1057
793
  }
1058
794
  return {
@@ -1064,23 +800,21 @@ export class RemoteK8sExecutionEnv {
1064
800
  };
1065
801
  },
1066
802
  async kill(state) {
1067
- await bgExec(buildBgKillScript(state.dir), true).catch(() => { }); // best-effort, idempotent
803
+ await bgExec(buildBgKillScript(state.dir), true).catch(() => { });
1068
804
  },
1069
805
  async disposeOne(state) {
1070
- // Adopted shells hold the (kept-open) exec socket — release it on EVERY dispose path, even when the pod
1071
- // is already being torn down (the socket is a worker-side resource, not a pod-side one).
1072
806
  const closeAdopted = () => {
1073
807
  if (!state.adopt)
1074
808
  return;
1075
809
  try {
1076
810
  (state.adopt.sock.terminate ?? state.adopt.sock.close).call(state.adopt.sock);
1077
811
  }
1078
- catch { /* already gone */ }
812
+ catch { }
1079
813
  };
1080
814
  if (self.destroyed) {
1081
815
  closeAdopted();
1082
816
  return;
1083
- } // pod teardown reaps the job; don't resurrect a connection
817
+ }
1084
818
  await bgExec(buildBgDisposeScript(state.dir), true).catch(() => { });
1085
819
  closeAdopted();
1086
820
  },
@@ -1096,11 +830,8 @@ export class RemoteK8sExecutionEnv {
1096
830
  return this.bgManager.kill(shellId);
1097
831
  }
1098
832
  disposeBackgroundShells(opts) {
1099
- // 签名统一带 opts(core 尾调 {except: keepAlive})。隔离 env 的留活是名义的——沙箱拆机进程必然随之
1100
- // 消亡(destroy 整体带走),core 的 envDying settle 会先给准确 killed 回执;透传保证签名不静默丢参。
1101
833
  return this._bgManager ? this._bgManager.dispose(opts) : Promise.resolve();
1102
834
  }
1103
- // ─────────────────────────────── FileSystem (over exec; real exit codes) ───────────────────────────────
1104
835
  async absolutePath(p) {
1105
836
  return ok(this.resolve(p));
1106
837
  }
@@ -1109,11 +840,9 @@ export class RemoteK8sExecutionEnv {
1109
840
  }
1110
841
  async readBinaryFile(p, abortSignal) {
1111
842
  const abs = this.resolve(p);
1112
- // base64 keeps the WS text-safe; the REAL exit code separates "empty file" from "read failed".
1113
843
  const r = await this.execData(`base64 < ${shellQuote(abs)}`, abortSignal);
1114
844
  if (!r.ok)
1115
845
  return { ok: false, error: fileErrorFromExec(r.error, abs) };
1116
- // Non-zero base64 read → central classifier, not_found fallback (truncated-stderr tolerant — council BUG#3).
1117
846
  if (r.value.exitCode !== 0)
1118
847
  return { ok: false, error: classifyFsStderr(r.value.stderr, abs, "not_found") };
1119
848
  return ok(new Uint8Array(Buffer.from(r.value.stdout.replace(/\s+/g, ""), "base64")));
@@ -1133,11 +862,6 @@ export class RemoteK8sExecutionEnv {
1133
862
  lines = lines.slice(0, options.maxLines);
1134
863
  return ok(lines);
1135
864
  }
1136
- /** 父目录自建(与 host/local-docker/adb/ssh 同口径)。抽成共用是因为 `appendFile` 曾**绕过**它:
1137
- * 🔴 2026-07-25 实测的跨腿分叉 —— `appendFile` 直接调 `writeChunked`,整条路上没有 `mkdir -p`,于是
1138
- * 「直接 append 建一个新日志路径」在 k8s 腿上返 `not_found`,而其余 5 腿都成功(host/local-docker/adb 实测,
1139
- * ssh/e2b 由方法体判据)。core 的后台输出镜像正是"每拍 append 一次"的形,这条分叉会让它在 k8s 腿上从第一拍
1140
- * 就失败,而 `mirrorFailed` 只会表现成一句「output file is INCOMPLETE」。 */
1141
865
  async ensureParentDir(abs, abortSignal) {
1142
866
  const dir = path.posix.dirname(abs);
1143
867
  if (!dir || dir === "." || dir === "/")
@@ -1154,21 +878,15 @@ export class RemoteK8sExecutionEnv {
1154
878
  const bad = await this.ensureParentDir(abs, abortSignal);
1155
879
  if (bad)
1156
880
  return bad;
1157
- return this.writeChunked(abs, content, true, abortSignal); // truncate first
881
+ return this.writeChunked(abs, content, true, abortSignal);
1158
882
  }
1159
883
  async appendFile(p, content, abortSignal) {
1160
884
  const abs = this.resolve(p);
1161
- const bad = await this.ensureParentDir(abs, abortSignal); // 见 ensureParentDir 旁注:此前这条路漏了它
885
+ const bad = await this.ensureParentDir(abs, abortSignal);
1162
886
  if (bad)
1163
887
  return bad;
1164
- return this.writeChunked(abs, content, false, abortSignal); // append from byte 0
888
+ return this.writeChunked(abs, content, false, abortSignal);
1165
889
  }
1166
- /**
1167
- * Write `content` to `abs` via argv-embedded base64, chunked at WRITE_CHUNK_BYTES (no stdin on the exec
1168
- * channel we use; v4 has no per-channel close). `truncateFirst` → the FIRST chunk uses `>` (overwrite),
1169
- * the rest `>>` (append); `false` → all chunks `>>`. Empty content still runs exactly one command so an
1170
- * empty file is created (write) / left intact (append). Shared by writeFile/appendFile (council DESIGN#2).
1171
- */
1172
890
  async writeChunked(abs, content, truncateFirst, abortSignal) {
1173
891
  const bytes = typeof content === "string" ? Buffer.from(content, "utf8") : Buffer.from(content);
1174
892
  for (let off = 0, first = true; off < bytes.length || first; off += WRITE_CHUNK_BYTES, first = false) {
@@ -1182,9 +900,6 @@ export class RemoteK8sExecutionEnv {
1182
900
  }
1183
901
  return ok(undefined);
1184
902
  }
1185
- // ── design/158 B6:POSIX shell-out 文件面全部委托共享基座(posix-shell-fs.ts)─────────────────────
1186
- // 六裁决(`--`/classifyFsStderr/ls -1A/mv 后缀形/temp 位置参数化/`[ -e ]||[ -L ]` exists)与
1187
- // readLink・lstat 契约见基座头注;本腿此前的 fileErrorFrom 分类形与 exists 形即基座正形的出处。
1188
903
  posixFsInst;
1189
904
  get posixFs() {
1190
905
  return (this.posixFsInst ??= createPosixShellFs({
@@ -1220,8 +935,6 @@ export class RemoteK8sExecutionEnv {
1220
935
  async createTempFile(options) {
1221
936
  return this.posixFs.createTempFile(options);
1222
937
  }
1223
- // ─────────────────────────────── helpers ───────────────────────────────
1224
- /** exec with the data-transfer bound (large reads/writes legitimately run longer than a control op). */
1225
938
  async execData(command, abortSignal) {
1226
939
  const c = await this.ensureConnected();
1227
940
  if (!c.ok)
@@ -1240,14 +953,7 @@ export class RemoteK8sExecutionEnv {
1240
953
  return parts.join(" ");
1241
954
  }
1242
955
  }
1243
- /**
1244
- * `ExecutionEnvFactory` for the k8s pod-sandbox backend — one fresh (unconnected) env per task; the pod is
1245
- * created lazily on connect and deleted by `destroy()` (Runner-owned lifetime; `activeDeadlineSeconds` is the
1246
- * leak guard if destroy never runs).
1247
- */
1248
956
  export function k8sExecutionEnvFactory(config) {
1249
- // Thread the task's sessionId so workspace-snapshot keys are namespaced per session
1250
- // (workspaces/<prefix>/<sessionId>/<snapshotId>.tar.gz).
1251
957
  return (ctx) => new RemoteK8sExecutionEnv({ ...config, sessionId: ctx.sessionId });
1252
958
  }
1253
959
  //# sourceMappingURL=remote-env-k8s.js.map