@sema-agent/server 7.138.0 → 7.139.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 (215) hide show
  1. package/MIGRATION.md +276 -7
  2. package/README.md +7 -4
  3. package/README.zh-CN.md +6 -4
  4. package/USAGE.md +73 -29
  5. package/deploy/sema-up/README.md +1 -1
  6. package/deploy/sema-up/lib/lanes.json +3 -2
  7. package/dist/adoption/wire.d.ts +3 -3
  8. package/dist/approval-card.d.ts +20 -20
  9. package/dist/bench/s1/arms.d.ts +1 -1
  10. package/dist/bench/s1/arms.js +1 -3
  11. package/dist/bench/s1/run-firm.js +2 -2
  12. package/dist/boot/device-lane.d.ts +8 -3
  13. package/dist/boot/device-lane.js +7 -2
  14. package/dist/boot/execution-env.d.ts +4 -2
  15. package/dist/boot/execution-env.js +16 -24
  16. package/dist/boot/execution-lane.d.ts +1 -1
  17. package/dist/boot/governance-seams.d.ts +3 -2
  18. package/dist/boot/governance-seams.js +9 -11
  19. package/dist/boot/memory-consolidation.js +2 -2
  20. package/dist/boot/resolve-spec.d.ts +1 -1
  21. package/dist/boot/resolve-spec.js +8 -9
  22. package/dist/boot/retention-lane.d.ts +5 -5
  23. package/dist/boot/retention-lane.js +0 -6
  24. package/dist/boot/runner-deps.d.ts +3 -1
  25. package/dist/boot/runner-deps.js +2 -1
  26. package/dist/boot/session-faces.d.ts +0 -3
  27. package/dist/boot/session-faces.js +15 -8
  28. package/dist/boot/stage-02-web-admin-pull.js +2 -2
  29. package/dist/boot/stage-05-execution-env.js +2 -2
  30. package/dist/boot/stage-06-runners.js +0 -1
  31. package/dist/boot/stage-09-leader.js +1 -2
  32. package/dist/boot/stage-10-http-server.js +2 -2
  33. package/dist/boot/stores.js +4 -4
  34. package/dist/boot/web-admin.js +2 -2
  35. package/dist/capabilities/mcp-probe.js +1 -1
  36. package/dist/capabilities/scenarios.d.ts +1 -1
  37. package/dist/capability-facts.d.ts +15 -7
  38. package/dist/capability-facts.js +8 -3
  39. package/dist/config-catalog.js +7 -7
  40. package/dist/config-invariants.d.ts +23 -3
  41. package/dist/config-invariants.js +42 -0
  42. package/dist/config-provider.d.ts +19 -24
  43. package/dist/config-provider.js +27 -12
  44. package/dist/config-types.d.ts +10 -5
  45. package/dist/config.d.ts +33 -0
  46. package/dist/config.js +44 -15
  47. package/dist/delegated-session-owner.d.ts +2 -2
  48. package/dist/deployment-shape.d.ts +1 -1
  49. package/dist/device-enrollment.d.ts +41 -2
  50. package/dist/device-enrollment.js +27 -12
  51. package/dist/device-store.d.ts +58 -85
  52. package/dist/device-store.js +19 -49
  53. package/dist/device-ws-hub.js +2 -1
  54. package/dist/env-name-allowlist-knobs.d.ts +5 -4
  55. package/dist/exec-cut.d.ts +60 -0
  56. package/dist/exec-cut.js +36 -0
  57. package/dist/execution-lane-caps.d.ts +19 -0
  58. package/dist/execution-lane-caps.js +10 -0
  59. package/dist/execution-placement.d.ts +259 -0
  60. package/dist/execution-placement.js +199 -0
  61. package/dist/field-conflict.d.ts +3 -2
  62. package/dist/field-conflict.js +1 -0
  63. package/dist/hosted-posture.d.ts +2 -2
  64. package/dist/http/admission.js +8 -19
  65. package/dist/http/checkpoint-ctx.d.ts +21 -12
  66. package/dist/http/checkpoint-ctx.js +4 -2
  67. package/dist/http/claimed-run.d.ts +1 -1
  68. package/dist/http/durable-run-leg.js +9 -5
  69. package/dist/http/execution-context.d.ts +218 -0
  70. package/dist/http/execution-context.js +330 -0
  71. package/dist/http/resume-legs.js +50 -20
  72. package/dist/http/route-ctx.d.ts +14 -8
  73. package/dist/http/routes/background.d.ts +29 -1
  74. package/dist/http/routes/background.js +29 -2
  75. package/dist/http/routes/devices.d.ts +13 -19
  76. package/dist/http/routes/devices.js +21 -26
  77. package/dist/http/routes/session-sync.js +110 -15
  78. package/dist/http/routes/sessions.d.ts +6 -0
  79. package/dist/http/routes/sessions.js +115 -11
  80. package/dist/http/routes/side-query.d.ts +12 -4
  81. package/dist/http/routes/side-query.js +4 -5
  82. package/dist/http/routes/tasks.js +37 -31
  83. package/dist/http/server.d.ts +35 -19
  84. package/dist/http/server.js +11 -0
  85. package/dist/http/session-inherit-env.d.ts +3 -4
  86. package/dist/http/session-inherit-env.js +1 -1
  87. package/dist/http/wire-types.d.ts +8 -12
  88. package/dist/index.d.ts +1 -0
  89. package/dist/memory-sync-client.d.ts +53 -14
  90. package/dist/memory-sync-client.js +196 -116
  91. package/dist/memory-sync.d.ts +1 -1
  92. package/dist/memory-sync.js +3 -2
  93. package/dist/observability/cost-taxonomy.d.ts +2 -2
  94. package/dist/observability/fail-open.d.ts +12 -0
  95. package/dist/observability/fail-open.js +12 -0
  96. package/dist/observability/run-terminal.js +0 -1
  97. package/dist/orchestration/workflow-notify-journal.js +4 -4
  98. package/dist/org-memory-admission.d.ts +4 -3
  99. package/dist/org-memory-admission.js +6 -6
  100. package/dist/parked-decide.d.ts +34 -9
  101. package/dist/parked-decide.js +12 -19
  102. package/dist/parked-revive-surface.d.ts +12 -15
  103. package/dist/parked-revive-surface.js +3 -6
  104. package/dist/peer-seat.d.ts +13 -7
  105. package/dist/peer-seat.js +37 -3
  106. package/dist/plugins/approval-ask-store-file.d.ts +7 -0
  107. package/dist/plugins/approval-ask-store-file.js +11 -0
  108. package/dist/plugins/approval-ask-store-memory.d.ts +6 -0
  109. package/dist/plugins/approval-ask-store-memory.js +12 -0
  110. package/dist/plugins/approval-ask-store-sql.d.ts +22 -5
  111. package/dist/plugins/approval-ask-store-sql.js +30 -12
  112. package/dist/plugins/caching-session-store.d.ts +6 -2
  113. package/dist/plugins/caching-session-store.js +9 -3
  114. package/dist/plugins/checkpoint-store-sql.d.ts +6 -5
  115. package/dist/plugins/checkpoint-store-sql.js +5 -7
  116. package/dist/plugins/device-store-sql.d.ts +25 -51
  117. package/dist/plugins/device-store-sql.js +49 -103
  118. package/dist/plugins/execution-placement-sql.d.ts +151 -0
  119. package/dist/plugins/execution-placement-sql.js +223 -0
  120. package/dist/plugins/file-history-store-sql.d.ts +7 -6
  121. package/dist/plugins/file-history-store-sql.js +5 -5
  122. package/dist/plugins/file-run-store.d.ts +6 -1
  123. package/dist/plugins/file-run-store.js +4 -0
  124. package/dist/plugins/fork-routing-session-store.d.ts +6 -0
  125. package/dist/plugins/fork-routing-session-store.js +16 -0
  126. package/dist/plugins/local-session-store.d.ts +149 -50
  127. package/dist/plugins/local-session-store.js +382 -107
  128. package/dist/plugins/memory-key-guards.d.ts +4 -0
  129. package/dist/plugins/memory-key-guards.js +1 -0
  130. package/dist/plugins/memory-run-store.d.ts +1 -1
  131. package/dist/plugins/one-time-migrations.d.ts +111 -3
  132. package/dist/plugins/one-time-migrations.js +380 -10
  133. package/dist/plugins/outcome-ledger-sql.d.ts +2 -2
  134. package/dist/plugins/permission-rule-store-sql.d.ts +13 -15
  135. package/dist/plugins/permission-rule-store-sql.js +13 -16
  136. package/dist/plugins/pg-pool.d.ts +7 -3
  137. package/dist/plugins/pg-session-storage.d.ts +31 -20
  138. package/dist/plugins/pg-session-storage.js +120 -52
  139. package/dist/plugins/remote-env-adb.d.ts +20 -4
  140. package/dist/plugins/remote-env-adb.js +70 -62
  141. package/dist/plugins/remote-env-device.d.ts +30 -12
  142. package/dist/plugins/remote-env-device.js +18 -17
  143. package/dist/plugins/remote-env-e2b.d.ts +18 -4
  144. package/dist/plugins/remote-env-e2b.js +41 -42
  145. package/dist/plugins/remote-env-file-error.d.ts +2 -0
  146. package/dist/plugins/remote-env-file-error.js +1 -1
  147. package/dist/plugins/remote-env-host.d.ts +21 -8
  148. package/dist/plugins/remote-env-host.js +66 -70
  149. package/dist/plugins/remote-env-k8s.d.ts +11 -16
  150. package/dist/plugins/remote-env-k8s.js +61 -54
  151. package/dist/plugins/remote-env-local-docker.d.ts +29 -6
  152. package/dist/plugins/remote-env-local-docker.js +46 -59
  153. package/dist/plugins/remote-env-ssh.d.ts +16 -4
  154. package/dist/plugins/remote-env-ssh.js +72 -56
  155. package/dist/plugins/remote-shell.d.ts +100 -3
  156. package/dist/plugins/remote-shell.js +166 -2
  157. package/dist/plugins/retention-store-sql.d.ts +9 -6
  158. package/dist/plugins/retention-store-sql.js +7 -1
  159. package/dist/plugins/run-store-sql.d.ts +13 -3
  160. package/dist/plugins/run-store-sql.js +5 -0
  161. package/dist/plugins/schema-consistency.d.ts +15 -5
  162. package/dist/plugins/schema-consistency.js +14 -8
  163. package/dist/plugins/schema-drift-sentence.d.ts +27 -0
  164. package/dist/plugins/schema-drift-sentence.js +38 -0
  165. package/dist/plugins/session-fork.d.ts +54 -0
  166. package/dist/plugins/session-fork.js +38 -1
  167. package/dist/plugins/session-row-sql.d.ts +22 -0
  168. package/dist/plugins/session-row-sql.js +81 -0
  169. package/dist/plugins/session-store.d.ts +1 -1
  170. package/dist/plugins/store-backend.d.ts +1 -1
  171. package/dist/plugins/store-backend.js +19 -7
  172. package/dist/plugins/store-contracts.d.ts +25 -5
  173. package/dist/plugins/tidb-pool.d.ts +12 -7
  174. package/dist/plugins/tidb-pool.js +9 -3
  175. package/dist/plugins/tidb-session-store.d.ts +57 -26
  176. package/dist/plugins/tidb-session-store.js +118 -50
  177. package/dist/plugins/tool-result-store-sql.d.ts +3 -2
  178. package/dist/plugins/tool-result-store-sql.js +18 -10
  179. package/dist/project-memory.d.ts +9 -15
  180. package/dist/project-memory.js +5 -4
  181. package/dist/request-key-closure.js +1 -1
  182. package/dist/request-value-closure.d.ts +6 -7
  183. package/dist/request-value-closure.js +1 -3
  184. package/dist/run-local.d.ts +6 -0
  185. package/dist/run-local.js +24 -20
  186. package/dist/runs.d.ts +2 -1
  187. package/dist/runs.js +3 -1
  188. package/dist/security.d.ts +38 -12
  189. package/dist/security.js +8 -2
  190. package/dist/server-secret-env.d.ts +9 -5
  191. package/dist/session-children-retire.d.ts +27 -9
  192. package/dist/session-children-retire.js +9 -5
  193. package/dist/session-incarnation.d.ts +19 -9
  194. package/dist/session-incarnation.js +4 -2
  195. package/dist/session-row.d.ts +114 -5
  196. package/dist/session-row.js +55 -5
  197. package/dist/session-titler.d.ts +2 -2
  198. package/dist/task-a2a.d.ts +1 -1
  199. package/dist/task-cwd.d.ts +6 -1
  200. package/dist/task-mcp.d.ts +3 -3
  201. package/dist/tool-approval.d.ts +6 -0
  202. package/dist/tool-approval.js +8 -1
  203. package/dist/trace/project.d.ts +1 -0
  204. package/dist/trace/project.js +1 -0
  205. package/dist/trace/self-minted-wire.d.ts +111 -2
  206. package/dist/trace/self-minted-wire.js +38 -1
  207. package/dist/url-refusal.d.ts +1 -1
  208. package/dist/web-admin-client/apply-effective.d.ts +17 -3
  209. package/dist/web-admin-client/apply-effective.js +20 -10
  210. package/dist/web-admin-client/skills-mcp.js +3 -2
  211. package/dist/web-admin-client/types.d.ts +6 -0
  212. package/package.json +3 -3
  213. package/wire-changes/server@7.139.0.json +348 -0
  214. package/dist/http/device-placement.d.ts +0 -175
  215. package/dist/http/device-placement.js +0 -145
@@ -76,11 +76,16 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
76
76
  * 谁该声明由闭集门看着(`test/execution-lane-caps.test.ts` 的逐 lane 对账:新 lane 不表态 = 编译红)。
77
77
  */
78
78
  /** `sharesProcessTable`(core 7.57.0,必填):命令是本进程 spawn 的子进程(同一张进程表),引擎的模式杀守卫对它照 NodeExecutionEnv 一样问 `pgrep`。 */
79
+ /** 能力块(引擎 [ref],八键必填、各键只经引擎读器读):`hostLocalPaths` = 这条车道答出的路径是不是本进程所在机器上的文件;`externalContentTarget` = 产出是否来自部署信任边界之外;`io` = 文件与 shell 两面真到达目标;`keepsReadFloor` = 是否守住读拒表的字节地板(本车道不守 ⇒ 读拒表非空的 run 被告知,部署要求地板时准备期拒)。 */
79
80
  readonly capabilities: {
80
- isolation: boolean;
81
- suspendable: boolean;
82
- canonicalPathAuthoritative: boolean;
83
- sharesProcessTable: boolean;
81
+ isolation: false;
82
+ suspendable: false;
83
+ canonicalPathAuthoritative: true;
84
+ sharesProcessTable: true;
85
+ hostLocalPaths: true;
86
+ externalContentTarget: false;
87
+ io: true;
88
+ keepsReadFloor: false;
84
89
  };
85
90
  /** R7 自唤醒: structural delegation to the injected scheduler backend. `hasScheduler(env)` checks BOTH the method
86
91
  * presence (always, since we implement the interface) AND `schedulerCapabilities.supported===true` — which is only
@@ -299,8 +304,9 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
299
304
  maxLines?: number;
300
305
  abortSignal?: AbortSignal;
301
306
  }): Promise<Result<string[], FileError>>;
302
- /** core `NodeExecutionEnv` 的绑定读器 —— 首次 {@link openBound} 时建(构造无副作用,只记 cwd)。 */
303
- private boundReader;
307
+ /** core `NodeExecutionEnv` 的一只实例 —— 本车道把**落盘那一步**委托给它的两只面({@link openBound} 的持有读、{@link writeFile} 的整文件替换);
308
+ * 首次用时建(构造无副作用,只记 cwd)。门(`fsReady`)、在飞登记(`withInflightFs`)与工作区相对解析留在本适配器。 */
309
+ private coreFs;
304
310
  /**
305
311
  * core 7.50.0 `FileSystem.openBound`:开着并**持有**一只对象(叶链接不跟,开后身份对拍),调用方判的与读的是同一只对象 ——
306
312
  * LSP 会话的读口与路径附件只经它读,不实现 ⇒ 两者在本车道一律拒。
@@ -310,8 +316,15 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
310
316
  */
311
317
  openBound(p: string, abortSignal?: AbortSignal): Promise<Result<BoundFileObject, FileError>>;
312
318
  readBinaryFile(p: string, abortSignal?: AbortSignal): Promise<Result<Uint8Array, FileError>>;
313
- /** [ref]:整只方法体(**含** `fsReady` 那道门)走 `withInflightFs` —— 登记必须与调用方同一同步段,
314
- * 否则 destroy 的快照可能刚好在 `await this.fsReady(...)` 的让出点上取完。见那处头注。 */
319
+ /**
320
+ * 整文件替换:**整旧或整新;`aborted` ⇒ 目标未碰**。
321
+ * 🔴 **不重写**:落盘那一步委托 core `NodeExecutionEnv.writeFile`(同目录暂存 + fsync + rename;父目录自建;单跳链接改写它指的
322
+ * 常规文件、链接链 / 非常规文件 / rename 不可用的文件系统回落就地写;已有文件的权限位带过去)—— 该原语的唯一属主在 core。
323
+ * 本适配器只给它与其余写面同一的入口纪律:[ref] 在飞登记(整只方法体**含** `fsReady` 那道门走 `withInflightFs` —— 登记必须与
324
+ * 调用方同一同步段,否则 destroy 的快照可能刚好在 `await this.fsReady(...)` 的让出点上取完;见那处头注)、销毁后拒、工作区相对解析。
325
+ * 修前这里是裸 `fs.writeFile(abs, data, { signal })`:open 即 O_TRUNC,中途取消 / 出错 ⇒ 目标 = 新内容的前缀,原件不可恢复。
326
+ * 已知语义变化(与 core / CC 同形):rename 断开硬链接 —— 另一条链接留旧内容。
327
+ */
315
328
  writeFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
316
329
  /** [ref](写面一律 `withInflightFs`,见那处头注)。本面是唯一能在 `destroyed` **之后**过门的写面
317
330
  * (`lingeringBgWrite`):那一族登记在 destroy 的快照**之后**,所以 destroy 不会去等它们 —— 正确,
@@ -7,6 +7,7 @@ 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";
@@ -37,7 +38,7 @@ const UNSUPPORTED_SCHEDULER_CAPS = {
37
38
  maxScheduledHorizonSec: 0,
38
39
  };
39
40
  export class RemoteHostExecutionEnv {
40
- capabilities = { isolation: false, suspendable: false, canonicalPathAuthoritative: true, sharesProcessTable: true };
41
+ capabilities = { isolation: false, suspendable: false, canonicalPathAuthoritative: true, sharesProcessTable: true, hostLocalPaths: true, externalContentTarget: false, io: true, keepsReadFloor: false };
41
42
  get schedulerCapabilities() {
42
43
  return this.cfg.scheduler?.schedulerCapabilities ?? UNSUPPORTED_SCHEDULER_CAPS;
43
44
  }
@@ -454,11 +455,11 @@ export class RemoteHostExecutionEnv {
454
455
  };
455
456
  const timer = setTimeout(() => {
456
457
  kill();
457
- finish({ ok: false, error: new ExecutionError("timeout", `host exec exceeded ${timeoutMs}ms`) });
458
+ finish({ ok: false, error: createCutError({ code: "timeout", message: `host exec exceeded ${timeoutMs}ms` }, buildCapturedOutput(outBuf, errBuf)) });
458
459
  }, timeoutMs);
459
460
  const onAbort = () => {
460
461
  kill();
461
- finish({ ok: false, error: new ExecutionError("aborted", "host exec aborted") });
462
+ finish({ ok: false, error: createCutError({ code: "aborted", message: "host exec aborted" }, buildCapturedOutput(outBuf, errBuf)) });
462
463
  };
463
464
  if (options?.abortSignal)
464
465
  options.abortSignal.addEventListener("abort", onAbort, { once: true });
@@ -607,32 +608,36 @@ export class RemoteHostExecutionEnv {
607
608
  }
608
609
  catch { }
609
610
  };
610
- const feed = (lane, inc) => {
611
+ const record = (lane, inc) => {
611
612
  lane.buf.push(inc);
612
- if (!settled) {
613
- try {
614
- lane.cb?.(inc.toString("utf8"));
615
- }
616
- catch { }
613
+ };
614
+ const deliver = (lane, inc) => {
615
+ try {
616
+ lane.cb?.(inc.toString("utf8"));
617
617
  }
618
+ catch { }
618
619
  };
619
- const pumpOnce = () => {
620
+ const feed = (lane, inc) => {
621
+ record(lane, inc);
622
+ deliver(lane, inc);
623
+ };
624
+ const pumpOnce = (take) => {
620
625
  for (const lane of lanes) {
621
626
  skipAhead(lane, ROTATE_BYTES);
622
627
  const inc = readIncrement(lane.path, lane.key);
623
628
  if (inc)
624
- feed(lane, inc);
629
+ take(lane, inc);
625
630
  reclaim(lane);
626
631
  }
627
632
  };
628
- const drainFinal = () => {
633
+ const drainFinal = (take) => {
629
634
  for (const lane of lanes) {
630
635
  skipAhead(lane, FINAL_TAIL_BYTES);
631
636
  for (let i = 0; i < 4; i++) {
632
637
  const inc = readIncrement(lane.path, lane.key);
633
638
  if (!inc)
634
639
  break;
635
- feed(lane, inc);
640
+ take(lane, inc);
636
641
  }
637
642
  }
638
643
  };
@@ -661,15 +666,17 @@ export class RemoteHostExecutionEnv {
661
666
  }
662
667
  };
663
668
  let removeDetachListener;
664
- const finish = (r) => {
669
+ const finish = (outcome) => {
665
670
  if (settled)
666
- return;
671
+ return false;
667
672
  settled = true;
673
+ const r = outcome();
668
674
  clearTimeout(timer);
669
675
  options?.abortSignal?.removeEventListener("abort", onAbort);
670
676
  removeDetachListener?.();
671
677
  cleanupSpool();
672
678
  resolve(r);
679
+ return true;
673
680
  };
674
681
  const closeSpoolFds = () => {
675
682
  for (const fd of spool.fds) {
@@ -697,66 +704,56 @@ export class RemoteHostExecutionEnv {
697
704
  ticket = adoptHostForegroundChild(child);
698
705
  closeSpoolFds();
699
706
  child.on("error", (e) => {
700
- finish({ ok: false, error: new ExecutionError("spawn_error", `host spawn failed: ${e.message}`) });
707
+ finish(() => ({ ok: false, error: new ExecutionError("spawn_error", `host spawn failed: ${e.message}`) }));
701
708
  });
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) };
709
+ const captured = () => {
710
+ pumpOnce(record);
711
+ return buildCapturedOutput(outBuf, errBuf);
707
712
  };
708
713
  let exited = false;
709
- const timer = setTimeout(() => {
714
+ const cut = (c) => {
710
715
  if (exited)
711
716
  return;
712
- try {
713
- if (child.pid != null)
714
- killTreeHard(child.pid);
715
- }
716
- catch {
717
+ finish(() => {
717
718
  try {
718
- child.kill("SIGKILL");
719
+ if (child.pid != null)
720
+ killTreeHard(child.pid);
719
721
  }
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");
722
+ catch {
723
+ try {
724
+ child.kill("SIGKILL");
725
+ }
726
+ catch { }
735
727
  }
736
- catch { }
737
- }
738
- finish({ ok: false, error: new ExecutionError("aborted", "host exec aborted") });
728
+ return { ok: false, error: createCutError(c, captured()) };
729
+ });
739
730
  };
731
+ const timer = setTimeout(() => cut({ code: "timeout", message: `host exec exceeded ${timeoutMs}ms` }), timeoutMs);
732
+ const onAbort = () => cut({ code: "aborted", message: "host exec aborted" });
740
733
  if (options?.abortSignal?.aborted) {
741
734
  onAbort();
742
735
  return;
743
736
  }
744
737
  if (options?.abortSignal)
745
738
  options.abortSignal.addEventListener("abort", onAbort, { once: true });
746
- pump = setInterval(pumpOnce, PUMP_MS);
739
+ pump = setInterval(() => pumpOnce(feed), PUMP_MS);
747
740
  pump.unref?.();
748
741
  const onDetach = () => {
749
742
  if (settled || exited)
750
743
  return;
751
- pumpOnce();
744
+ pumpOnce(feed);
745
+ if (settled)
746
+ return;
752
747
  const fgDropped = spoolRemoved.out + spoolRemoved.err;
753
748
  const shellId = this.adoptRunningExecSpool(child, spool, { out: 0, err: 0 }, fgDropped);
754
749
  if (shellId === undefined)
755
750
  return;
756
751
  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 } }));
752
+ finish(() => {
753
+ const o = outBuf.result();
754
+ const e = errBuf.result();
755
+ return ok({ stdout: markTruncated(o.text, o.droppedBytes), stderr: markTruncated(e.text, e.droppedBytes), exitCode: 0, detached: { shellId } });
756
+ });
760
757
  };
761
758
  if (detachSignal) {
762
759
  if (detachSignal.aborted)
@@ -767,13 +764,20 @@ export class RemoteHostExecutionEnv {
767
764
  }
768
765
  }
769
766
  const settleFromTermination = (code, signal) => {
770
- if (settled)
771
- return;
772
- drainFinal();
773
- const o = outBuf.result();
774
- const e = errBuf.result();
775
767
  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 }));
768
+ const drained = [];
769
+ const won = finish(() => {
770
+ drainFinal((lane, inc) => {
771
+ record(lane, inc);
772
+ drained.push({ lane, inc });
773
+ });
774
+ const o = outBuf.result();
775
+ const e = errBuf.result();
776
+ return ok({ stdout: markTruncated(o.text, o.droppedBytes), stderr: markTruncated(e.text, e.droppedBytes), exitCode });
777
+ });
778
+ if (won)
779
+ for (const d of drained)
780
+ deliver(d.lane, d.inc);
777
781
  };
778
782
  child.on("close", (code, signal) => {
779
783
  if (drainTimer) {
@@ -972,13 +976,13 @@ export class RemoteHostExecutionEnv {
972
976
  lines = lines.slice(0, options.maxLines);
973
977
  return ok(lines);
974
978
  }
975
- boundReader;
979
+ coreFs;
976
980
  async openBound(p, abortSignal) {
977
981
  const ready = await this.fsReady(p);
978
982
  if (!ready.ok)
979
983
  return ready;
980
- this.boundReader ??= new NodeExecutionEnv({ cwd: this.workspaceDir });
981
- return this.boundReader.openBound(this.resolve(p), abortSignal);
984
+ this.coreFs ??= new NodeExecutionEnv({ cwd: this.workspaceDir });
985
+ return this.coreFs.openBound(this.resolve(p), abortSignal);
982
986
  }
983
987
  async readBinaryFile(p, abortSignal) {
984
988
  const ready = await this.fsReady(p);
@@ -998,16 +1002,8 @@ export class RemoteHostExecutionEnv {
998
1002
  const ready = await this.fsReady(p);
999
1003
  if (!ready.ok)
1000
1004
  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
- }
1005
+ this.coreFs ??= new NodeExecutionEnv({ cwd: this.workspaceDir });
1006
+ return this.coreFs.writeFile(this.resolve(p), content, abortSignal);
1011
1007
  });
1012
1008
  }
1013
1009
  appendFile(p, content, abortSignal) {
@@ -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
@@ -301,12 +301,8 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
301
301
  * 谁该声明由闭集门看着(`test/execution-lane-caps.test.ts` 的逐 lane 对账:新 lane 不表态 = 编译红)。
302
302
  */
303
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
- };
304
+ /** 能力块(引擎 [ref],八键必填、各键只经引擎读器读):`hostLocalPaths` = 这条车道答出的路径是不是本进程所在机器上的文件;`externalContentTarget` = 产出是否来自部署信任边界之外;`io` = 文件与 shell 两面真到达目标;`keepsReadFloor` = 是否守住读拒表的字节地板(本车道不守 ⇒ 读拒表非空的 run 被告知,部署要求地板时准备期拒)。 */
305
+ readonly capabilities: ExecutionEnvCapabilities;
310
306
  cwd: string;
311
307
  private podName?;
312
308
  /** Cluster-internal pod address, captured when the pod reaches Running — the LSP `getHost` surface.
@@ -813,19 +809,18 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
813
809
  maxLines?: number;
814
810
  abortSignal?: AbortSignal;
815
811
  }): 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
812
  writeFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
813
+ /** 暂存写的两只面(规则与提交脚本的唯一属主 = `remote-shell.ts`;父目录与私有暂存目录由编排的准备那一步建):上传 = 分块整份写进私有目录里的暂存件;
814
+ * 提交 = 文件面探针 exec。两者同一身份(同一只容器里的 exec)。 */
815
+ private stagedLane;
823
816
  appendFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
824
817
  /**
825
818
  * Write `content` to `abs` via argv-embedded base64, chunked at WRITE_CHUNK_BYTES (no stdin on the exec
826
819
  * channel we use; v4 has no per-channel close). `truncateFirst` → the FIRST chunk uses `>` (overwrite),
827
820
  * the rest `>>` (append); `false` → all chunks `>>`. Empty content still runs exactly one command so an
828
821
  * empty file is created (write) / left intact (append). Shared by writeFile/appendFile (council DESIGN#2).
822
+ * Both file faces point this at a STAGING name (`writeFileStaged` / `appendFileStaged`), so a cut mid-way never reaches
823
+ * 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
824
  */
830
825
  private writeChunked;
831
826
  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,6 +15,7 @@ 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";
20
21
  import { fileErrorFromExec, classifyFsStderr } from "./remote-env-file-error.js";
@@ -171,7 +172,6 @@ export function errorMessageOf(e) {
171
172
  return typeof m === "string" && m ? m : String(e);
172
173
  }
173
174
  export { exitCodeFromStatus } from "./k8s-exec-protocol.js";
174
- export const K8S_SANDBOX_NAMESPACE_DEFAULT = "sandboxes";
175
175
  const SANDBOX_SECRET_NAME_PREFIX = "sbx-env-";
176
176
  const SANDBOX_SECRET_LABEL = "sema.io/sandbox-secret";
177
177
  const secretEnvKey = (name) => `env.${name}`;
@@ -235,7 +235,7 @@ export class RemoteK8sExecutionEnv {
235
235
  token: config.token,
236
236
  caCert: config.caCert,
237
237
  insecureTls: config.insecureTls ?? false,
238
- namespace: config.namespace ?? K8S_SANDBOX_NAMESPACE_DEFAULT,
238
+ namespace: config.namespace,
239
239
  image: config.image,
240
240
  runtimeClass: config.runtimeClass ?? "kata-qemu",
241
241
  mountPath: config.mountPath ?? K8S_WORKSPACE_MOUNT_DEFAULT,
@@ -268,7 +268,7 @@ export class RemoteK8sExecutionEnv {
268
268
  this.logger = config.logger;
269
269
  this.onWorkspaceSnapshot = config.onWorkspaceSnapshot;
270
270
  this.cacheMisses = config.cacheMisses;
271
- this.capabilities = { isolation: true, suspendable: !!this.cfg.s3Snapshot, canonicalPathAuthoritative: true, sharesProcessTable: false };
271
+ this.capabilities = { isolation: true, suspendable: !!this.cfg.s3Snapshot, canonicalPathAuthoritative: true, sharesProcessTable: false, hostLocalPaths: false, externalContentTarget: false, io: true, keepsReadFloor: false };
272
272
  const bgMax = Math.max(1, Math.min(K8S_BG_MAX_TIMEOUT_SEC, Math.floor(this.cfg.timeoutMs / 1000)));
273
273
  this.backgroundCapabilities = {
274
274
  supported: true,
@@ -1498,10 +1498,11 @@ export class RemoteK8sExecutionEnv {
1498
1498
  let sinkClosed = false;
1499
1499
  let removeDetachListener;
1500
1500
  const detachTee = opts?.detach?.tee;
1501
- const finish = (r) => {
1501
+ const finish = (outcome) => {
1502
1502
  if (settled)
1503
- return;
1503
+ return false;
1504
1504
  settled = true;
1505
+ const r = outcome();
1505
1506
  clearTimeout(wallTimer);
1506
1507
  clearTimeout(livenessTimer);
1507
1508
  opts?.signal?.removeEventListener("abort", onAbort);
@@ -1512,11 +1513,37 @@ export class RemoteK8sExecutionEnv {
1512
1513
  catch {
1513
1514
  }
1514
1515
  resolvePromise(r);
1516
+ return true;
1515
1517
  };
1516
1518
  const bounds = commandBoundsOf({ timeoutMs: opts?.timeoutMs, readTimeoutMs: opts?.readTimeoutMs }, this.laneLiveness);
1517
1519
  const capMs = opts?.capMs;
1518
1520
  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;
1521
+ const recordStderr = (bytes) => {
1522
+ if (bytes.length === 0)
1523
+ return;
1524
+ errBuf.push(bytes);
1525
+ if (detachTee)
1526
+ feedMemStream(detachTee.err, bytes, K8S_BG_MEM_CAP);
1527
+ };
1528
+ const emitStderr = (bytes) => {
1529
+ if (bytes.length > 0 && errDecoder)
1530
+ opts.onStderr(errDecoder.write(bytes));
1531
+ };
1532
+ const takeStderr = (bytes) => {
1533
+ recordStderr(bytes);
1534
+ emitStderr(bytes);
1535
+ };
1536
+ const heldStderr = () => opts?.stderrFilter?.endStderr();
1537
+ const captured = () => {
1538
+ const held = heldStderr();
1539
+ if (held)
1540
+ recordStderr(held);
1541
+ return buildCapturedOutput(outTail, errBuf);
1542
+ };
1543
+ const cut = (c) => {
1544
+ finish(() => ({ ok: false, error: createCutError(c, captured()) }));
1545
+ };
1546
+ const wallTimer = wallMs !== undefined ? setTimeout(() => cut({ code: "timeout", message: `command exceeded ${wallMs}ms` }), wallMs) : undefined;
1520
1547
  let livenessTimer;
1521
1548
  const liveness = bounds.liveness;
1522
1549
  const armLiveness = () => {
@@ -1525,28 +1552,18 @@ export class RemoteK8sExecutionEnv {
1525
1552
  clearTimeout(livenessTimer);
1526
1553
  const idle = Date.now() - lastProgress;
1527
1554
  if (idle >= liveness.ms) {
1528
- const cut = buildLivenessCut(liveness);
1529
- finish({ ok: false, error: new ExecutionError(cut.code, cut.message, undefined, undefined, cut.noOutputSec) });
1555
+ cut(buildLivenessCut(liveness));
1530
1556
  return;
1531
1557
  }
1532
1558
  livenessTimer = setTimeout(armLiveness, liveness.ms - idle);
1533
1559
  };
1534
1560
  armLiveness();
1535
- const onAbort = () => finish({ ok: false, error: new ExecutionError("aborted", "command aborted") });
1561
+ const onAbort = () => cut({ code: "aborted", message: "command aborted" });
1536
1562
  if (opts?.signal) {
1537
1563
  if (opts.signal.aborted)
1538
1564
  return onAbort();
1539
1565
  opts.signal.addEventListener("abort", onAbort, { once: true });
1540
1566
  }
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
1567
  sock.on("message", (...args) => {
1551
1568
  const data = args[0];
1552
1569
  const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
@@ -1588,7 +1605,7 @@ export class RemoteK8sExecutionEnv {
1588
1605
  }
1589
1606
  return;
1590
1607
  }
1591
- finish({ ok: false, error: new ExecutionError("transport_lost", `exec socket error: ${errorMessageOf(e)}`, e instanceof Error ? e : undefined) });
1608
+ finish(() => ({ ok: false, error: new ExecutionError("transport_lost", `exec socket error: ${errorMessageOf(e)}`, e instanceof Error ? e : undefined) }));
1592
1609
  });
1593
1610
  sock.on("close", () => {
1594
1611
  if (spliceSink) {
@@ -1601,25 +1618,30 @@ export class RemoteK8sExecutionEnv {
1601
1618
  return spliceSink.closed("error" in parsed ? { failed: true } : { failed: false, exitCode: parsed.exitCode });
1602
1619
  }
1603
1620
  if (statusFrame === undefined) {
1604
- finish({ ok: false, error: new ExecutionError("transport_lost", "exec socket closed without status — k8s transport lost mid-command") });
1621
+ finish(() => ({ ok: false, error: new ExecutionError("transport_lost", "exec socket closed without status — k8s transport lost mid-command") }));
1605
1622
  return;
1606
1623
  }
1607
1624
  const parsed = exitCodeFromStatus(statusFrame);
1608
1625
  if ("error" in parsed) {
1609
- finish({ ok: false, error: new ExecutionError("spawn_error", parsed.error) });
1626
+ const said = parsed.error;
1627
+ finish(() => ({ ok: false, error: new ExecutionError("spawn_error", said) }));
1610
1628
  return;
1611
1629
  }
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
- }
1630
+ const exitCode = parsed.exitCode;
1631
+ const held = heldStderr();
1632
+ const won = finish(() => {
1633
+ if (held)
1634
+ recordStderr(held);
1635
+ const err = errBuf.result();
1636
+ const stderr = markTruncated(err.text, err.droppedBytes);
1637
+ if (boundOutput) {
1638
+ const out = outTail.result();
1639
+ return ok({ stdout: markTruncated(out.text, out.droppedBytes), stderr, exitCode });
1640
+ }
1641
+ return ok({ stdout: Buffer.concat(outBufs).toString("utf8"), stderr, exitCode });
1642
+ });
1643
+ if (won && held)
1644
+ emitStderr(held);
1623
1645
  });
1624
1646
  const detach = opts?.detach;
1625
1647
  if (detach) {
@@ -1925,30 +1947,15 @@ export class RemoteK8sExecutionEnv {
1925
1947
  lines = lines.slice(0, options.maxLines);
1926
1948
  return ok(lines);
1927
1949
  }
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
1950
  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);
1951
+ return writeFileStaged(this.stagedLane(content), this.resolve(p), abortSignal);
1952
+ }
1953
+ stagedLane(content) {
1954
+ return { upload: (dest, signal) => this.writeChunked(dest, content, true, signal), run: (cmd, signal) => this.fsExec(cmd, signal) };
1945
1955
  }
1946
1956
  async appendFile(p, content, abortSignal) {
1947
1957
  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);
1958
+ return appendFileStaged(this.stagedLane(content), abs, abortSignal, (ctx) => ctx.inPlace((signal) => this.writeChunked(abs, content, false, signal)));
1952
1959
  }
1953
1960
  async writeChunked(abs, content, truncateFirst, abortSignal) {
1954
1961
  const bytes = typeof content === "string" ? Buffer.from(content, "utf8") : Buffer.from(content);
@@ -2119,7 +2126,7 @@ export function createSandboxPodApi(config, deps) {
2119
2126
  let env;
2120
2127
  const control = () => (env ??= new RemoteK8sExecutionEnv(config, deps));
2121
2128
  return {
2122
- namespace: config.namespace ?? K8S_SANDBOX_NAMESPACE_DEFAULT,
2129
+ namespace: config.namespace,
2123
2130
  list: () => control().listSandboxPods(),
2124
2131
  delete: (name) => control().deletePod(name),
2125
2132
  };