@sema-agent/server 7.141.0 → 7.142.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 (171) hide show
  1. package/MIGRATION.md +215 -0
  2. package/USAGE.md +67 -12
  3. package/dist/approval-ask-audit-store.d.ts +54 -38
  4. package/dist/approval-ask-audit-store.js +58 -39
  5. package/dist/approval-card.d.ts +8 -8
  6. package/dist/approval-crash-convergence.d.ts +17 -7
  7. package/dist/approval-crash-convergence.js +94 -22
  8. package/dist/approval-deny-reasons.d.ts +34 -30
  9. package/dist/approval-deny-reasons.js +21 -6
  10. package/dist/approval-policy-settlement.d.ts +1 -1
  11. package/dist/approval-reconciler.d.ts +33 -1
  12. package/dist/approval-reconciler.js +43 -40
  13. package/dist/auth-bridge.d.ts +9 -6
  14. package/dist/auth-bridge.js +9 -3
  15. package/dist/boot/adoption.js +2 -1
  16. package/dist/boot/budget-tracing.d.ts +16 -0
  17. package/dist/boot/budget-tracing.js +10 -6
  18. package/dist/boot/device-lane.d.ts +1 -1
  19. package/dist/boot/execution-env.d.ts +24 -4
  20. package/dist/boot/execution-env.js +26 -23
  21. package/dist/boot/execution-lane.js +2 -1
  22. package/dist/boot/exit-boundary.js +5 -4
  23. package/dist/boot/memory-consolidation.js +2 -1
  24. package/dist/boot/parked-revive-gate.js +2 -1
  25. package/dist/boot/permission-rules-audit.d.ts +0 -28
  26. package/dist/boot/permission-rules-audit.js +2 -1
  27. package/dist/boot/read-deny-anchor-audit.js +2 -1
  28. package/dist/boot/reapers.js +4 -3
  29. package/dist/boot/resolve-spec.js +11 -2
  30. package/dist/boot/retention-lane.js +5 -3
  31. package/dist/boot/runner-deps.js +16 -13
  32. package/dist/boot/runtime-caps.js +3 -2
  33. package/dist/boot/session-faces.js +4 -3
  34. package/dist/boot/shutdown.js +3 -2
  35. package/dist/boot/stage-04-stores.js +3 -2
  36. package/dist/boot/stage-05-execution-env.d.ts +1 -1
  37. package/dist/boot/stage-05-execution-env.js +8 -6
  38. package/dist/boot/stage-06-runners.d.ts +1 -1
  39. package/dist/boot/stage-06-runners.js +2 -1
  40. package/dist/boot/stage-07-capability-layer.d.ts +1 -1
  41. package/dist/boot/stage-07-capability-layer.js +2 -1
  42. package/dist/boot/stage-08-reapers.d.ts +1 -1
  43. package/dist/boot/stage-08-reapers.js +3 -2
  44. package/dist/boot/stage-09-leader.d.ts +1 -1
  45. package/dist/boot/stage-10-http-server.d.ts +1 -1
  46. package/dist/boot/stage-10-http-server.js +5 -4
  47. package/dist/boot/stores.js +3 -2
  48. package/dist/boot/web-admin.js +17 -17
  49. package/dist/boot/webfetch-summarize-lane.js +2 -1
  50. package/dist/boot/workflow-orchestration.js +3 -2
  51. package/dist/capabilities/builtin-tools.d.ts +2 -1
  52. package/dist/capabilities/builtin-tools.js +2 -4
  53. package/dist/capabilities/composite-members.d.ts +2 -1
  54. package/dist/capabilities/composite-members.js +2 -1
  55. package/dist/capability-facts.d.ts +24 -0
  56. package/dist/capability-facts.js +23 -0
  57. package/dist/config-catalog.js +1 -0
  58. package/dist/config-types.d.ts +15 -4
  59. package/dist/config.d.ts +6 -0
  60. package/dist/config.js +17 -3
  61. package/dist/execution-lane-caps.d.ts +2 -1
  62. package/dist/fleet/fleet-bus.d.ts +6 -3
  63. package/dist/hooks/hook-runner.d.ts +6 -0
  64. package/dist/hooks/hook-runner.js +30 -1
  65. package/dist/http/dispatch.js +4 -2
  66. package/dist/http/execution-context.d.ts +2 -2
  67. package/dist/http/idempotency.d.ts +1 -1
  68. package/dist/http/principal-gate.d.ts +3 -1
  69. package/dist/http/principal-gate.js +17 -8
  70. package/dist/http/resume-legs.js +5 -9
  71. package/dist/http/route-table.js +2 -0
  72. package/dist/http/routes/attachments.js +18 -0
  73. package/dist/http/routes/capabilities.js +5 -2
  74. package/dist/http/routes/memory-policy.js +26 -6
  75. package/dist/http/routes/runs.js +3 -3
  76. package/dist/http/routes/session-memory-sync.d.ts +63 -0
  77. package/dist/http/routes/session-memory-sync.js +157 -0
  78. package/dist/http/routes/sessions-list.d.ts +2 -1
  79. package/dist/http/routes/tasks.js +7 -5
  80. package/dist/http/run-read-face.d.ts +7 -0
  81. package/dist/http/run-read-face.js +9 -3
  82. package/dist/http/send.d.ts +5 -0
  83. package/dist/http/send.js +9 -2
  84. package/dist/http/server.d.ts +14 -8
  85. package/dist/http/server.js +2 -4
  86. package/dist/http/submit-replay.d.ts +4 -3
  87. package/dist/http/submit-replay.js +2 -2
  88. package/dist/http/sync-submit-body.d.ts +21 -4
  89. package/dist/http/sync-submit-body.js +13 -0
  90. package/dist/identity-source.d.ts +21 -2
  91. package/dist/identity-source.js +6 -0
  92. package/dist/memory-incident-sink.d.ts +19 -6
  93. package/dist/memory-incident-sink.js +8 -4
  94. package/dist/memory-sync-client.d.ts +21 -21
  95. package/dist/memory-sync-client.js +50 -42
  96. package/dist/memory-sync-identity.d.ts +24 -0
  97. package/dist/memory-sync-identity.js +57 -0
  98. package/dist/memory-sync.d.ts +7 -1
  99. package/dist/memory-sync.js +6 -3
  100. package/dist/observability/fail-open.d.ts +27 -10
  101. package/dist/observability/fail-open.js +20 -7
  102. package/dist/observability/run-terminal.d.ts +92 -34
  103. package/dist/observability/run-terminal.js +167 -162
  104. package/dist/plugins/approval-ask-store-file.d.ts +14 -4
  105. package/dist/plugins/approval-ask-store-file.js +48 -7
  106. package/dist/plugins/approval-ask-store-memory.d.ts +2 -1
  107. package/dist/plugins/approval-ask-store-memory.js +9 -8
  108. package/dist/plugins/approval-ask-store-sql.d.ts +42 -10
  109. package/dist/plugins/approval-ask-store-sql.js +49 -19
  110. package/dist/plugins/file-run-store.d.ts +6 -1
  111. package/dist/plugins/file-run-store.js +11 -1
  112. package/dist/plugins/local-session-store.d.ts +36 -7
  113. package/dist/plugins/local-session-store.js +76 -32
  114. package/dist/plugins/local-task-attachment-store.d.ts +12 -2
  115. package/dist/plugins/local-task-attachment-store.js +67 -43
  116. package/dist/plugins/memory-key-guards.d.ts +3 -3
  117. package/dist/plugins/memory-run-store.d.ts +6 -1
  118. package/dist/plugins/memory-run-store.js +11 -1
  119. package/dist/plugins/one-time-migrations.d.ts +27 -1
  120. package/dist/plugins/one-time-migrations.js +57 -3
  121. package/dist/plugins/remote-env-e2b.d.ts +23 -4
  122. package/dist/plugins/remote-env-e2b.js +111 -28
  123. package/dist/plugins/remote-env-k8s.d.ts +31 -5
  124. package/dist/plugins/remote-env-k8s.js +86 -19
  125. package/dist/plugins/remote-env-local-docker.d.ts +15 -3
  126. package/dist/plugins/remote-env-local-docker.js +40 -12
  127. package/dist/plugins/run-store-sql.d.ts +9 -1
  128. package/dist/plugins/run-store-sql.js +9 -1
  129. package/dist/plugins/store-backend.d.ts +10 -7
  130. package/dist/plugins/store-backend.js +1 -0
  131. package/dist/plugins/store-contracts.d.ts +26 -1
  132. package/dist/plugins/store-contracts.js +3 -0
  133. package/dist/plugins/task-attachment-store.d.ts +112 -14
  134. package/dist/plugins/task-attachment-store.js +221 -28
  135. package/dist/plugins/workspace-fill.d.ts +60 -0
  136. package/dist/plugins/workspace-fill.js +106 -0
  137. package/dist/project-bundle.d.ts +38 -0
  138. package/dist/project-bundle.js +609 -0
  139. package/dist/run-local.js +2 -8
  140. package/dist/runs.js +1 -2
  141. package/dist/sandbox-egress-policy.d.ts +4 -1
  142. package/dist/sandbox-egress-policy.js +3 -1
  143. package/dist/sandbox-git-plane.d.ts +31 -12
  144. package/dist/sandbox-git-plane.js +24 -4
  145. package/dist/security.d.ts +9 -0
  146. package/dist/security.js +8 -0
  147. package/dist/session-error-wire.d.ts +24 -0
  148. package/dist/session-error-wire.js +7 -0
  149. package/dist/task-settings.d.ts +10 -2
  150. package/dist/task-settings.js +15 -2
  151. package/dist/terminal.d.ts +39 -19
  152. package/dist/terminal.js +65 -22
  153. package/dist/tool-approval.d.ts +4 -3
  154. package/dist/tool-approval.js +12 -14
  155. package/dist/trace/core-keyset-guard.d.ts +123 -6
  156. package/dist/trace/engine-notice-wire.d.ts +8 -2
  157. package/dist/trace/engine-notice-wire.js +4 -2
  158. package/dist/trace/exit-seat.d.ts +2 -1
  159. package/dist/trace/ledger-sink.d.ts +8 -4
  160. package/dist/trace/ledger-sink.js +2 -2
  161. package/dist/trace/project.d.ts +18 -1
  162. package/dist/trace/project.js +16 -0
  163. package/dist/trace/self-minted-wire.d.ts +251 -2
  164. package/dist/trace/self-minted-wire.js +82 -0
  165. package/dist/trace/wire-projection-faces.d.ts +1 -1
  166. package/dist/trace/wire-projection-faces.js +1 -1
  167. package/dist/web-admin-client/apply-effective.js +21 -1
  168. package/dist/web-admin-client/effective-keys.js +2 -0
  169. package/dist/web-admin-client/types.d.ts +4 -0
  170. package/package.json +1 -1
  171. package/wire-changes/server@7.142.0.json +639 -0
@@ -35,7 +35,7 @@ export class RemoteContainerExecutionEnv {
35
35
  handle;
36
36
  destroyed = false;
37
37
  homeSeat;
38
- homeRefusal;
38
+ connectRefusal;
39
39
  connecting;
40
40
  activeCommands = 0;
41
41
  paused = false;
@@ -138,10 +138,7 @@ export class RemoteContainerExecutionEnv {
138
138
  };
139
139
  }
140
140
  workspaceHandle() {
141
- if (!this.handle) {
142
- throw new RemoteExecutionError("connect_failed", "workspaceHandle() called before connect()");
143
- }
144
- return this.handle;
141
+ return this.handle ?? { sandboxId: "(unconnected)", provider: PROVIDER, mountPath: this.cwd };
145
142
  }
146
143
  async getHost(port) {
147
144
  const conn = await this.ensureConnected();
@@ -162,17 +159,18 @@ export class RemoteContainerExecutionEnv {
162
159
  }
163
160
  if (this.destroyed)
164
161
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env already destroyed") };
165
- if (this.homeRefusal)
166
- return { ok: false, error: this.homeRefusal };
162
+ if (this.connectRefusal)
163
+ return { ok: false, error: this.connectRefusal };
167
164
  if (this.paused)
168
165
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env is suspended (paused) — resumeVM(snapshotId) before use") };
169
166
  if (this.sbx)
170
167
  return ok(this.workspaceHandle());
171
168
  if (this.connecting)
172
- return this.connecting;
173
- this.connecting = this.doConnect(config);
174
- const r = await this.connecting;
175
- if (!r.ok)
169
+ return this.connecting.promise;
170
+ const mine = { target: undefined, promise: this.doConnect(config) };
171
+ this.connecting = mine;
172
+ const r = await mine.promise;
173
+ if (!r.ok && this.connecting === mine)
176
174
  this.connecting = undefined;
177
175
  return r;
178
176
  }
@@ -201,6 +199,14 @@ export class RemoteContainerExecutionEnv {
201
199
  await sbx.kill().catch(() => { });
202
200
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env destroyed during connect") };
203
201
  }
202
+ const filled = await this.fillAt(sbx, this.cfg.mountPath, "minted");
203
+ if (!filled.ok || this.destroyed) {
204
+ await sbx.kill().catch(() => { });
205
+ this.cfg.metrics?.inc("remote_env_connect_total", { provider: PROVIDER, result: "error" });
206
+ if (!filled.ok && filled.standing)
207
+ this.connectRefusal = filled.error;
208
+ return { ok: false, error: filled.ok ? new RemoteExecutionError("connect_failed", "execution env destroyed during connect") : filled.error };
209
+ }
204
210
  this.sbx = sbx;
205
211
  this.paused = false;
206
212
  this.startKeepAlive();
@@ -250,20 +256,50 @@ export class RemoteContainerExecutionEnv {
250
256
  return { ok: false, error: this.remoteError(e, options?.abortSignal?.aborted ? "aborted" : "unknown") };
251
257
  }
252
258
  }
253
- async resumeVM(snapshotId, options) {
259
+ resumeVM(snapshotId, options) {
254
260
  if (this.destroyed)
255
- return { ok: false, error: new RemoteExecutionError("connect_failed", "env already destroyed") };
256
- if (this.homeRefusal)
257
- return { ok: false, error: this.homeRefusal };
261
+ return Promise.resolve({ ok: false, error: new RemoteExecutionError("connect_failed", "env already destroyed") });
262
+ if (this.connectRefusal)
263
+ return Promise.resolve({ ok: false, error: this.connectRefusal });
258
264
  if (this.sbx && !this.paused && this.sbx.sandboxId === snapshotId)
259
- return ok(this.workspaceHandle());
260
- if (this.connecting)
261
- return this.connecting;
262
- this.connecting = this.doResumeVM(snapshotId, options);
263
- const r = await this.connecting;
264
- if (!r.ok)
265
- this.connecting = undefined;
266
- return r;
265
+ return Promise.resolve(ok(this.workspaceHandle()));
266
+ const inFlight = this.connecting;
267
+ if (inFlight?.target === snapshotId)
268
+ return inFlight.promise;
269
+ const mine = { target: snapshotId, promise: this.resumeWalk(inFlight, snapshotId, options) };
270
+ this.connecting = mine;
271
+ const forget = () => {
272
+ if (this.connecting === mine)
273
+ this.connecting = undefined;
274
+ };
275
+ mine.promise.then((r) => {
276
+ if (!r.ok)
277
+ forget();
278
+ }, forget);
279
+ return mine.promise;
280
+ }
281
+ async resumeWalk(inFlight, snapshotId, options) {
282
+ if (inFlight !== undefined) {
283
+ await inFlight.promise;
284
+ if (this.destroyed)
285
+ return { ok: false, error: new RemoteExecutionError("connect_failed", "env already destroyed") };
286
+ if (this.sbx && !this.paused && this.sbx.sandboxId === snapshotId)
287
+ return ok(this.workspaceHandle());
288
+ }
289
+ if (this.sbx && !this.paused)
290
+ await this.releaseMinted(snapshotId);
291
+ return this.doResumeVM(snapshotId, options);
292
+ }
293
+ async releaseMinted(resumed) {
294
+ const sbx = this.sbx;
295
+ if (sbx === undefined)
296
+ return;
297
+ this.sbx = undefined;
298
+ this.handle = undefined;
299
+ this.stopKeepAlive();
300
+ await this.disposeBackgroundShells();
301
+ await this.rpc("sandbox.kill", () => sbx.kill()).catch((e) => this.cfg.logger?.warn("remote_env_minted_release_failed", { sandboxId: sbx.sandboxId, error: errMsg(e) }));
302
+ this.cfg.logger?.info?.("remote_env_minted_released", { sandboxId: sbx.sandboxId, resumed });
267
303
  }
268
304
  async doResumeVM(snapshotId, options) {
269
305
  try {
@@ -288,6 +324,18 @@ export class RemoteContainerExecutionEnv {
288
324
  this.cfg.metrics?.inc("remote_env_resume_total", { provider: PROVIDER, result: "error" });
289
325
  return { ok: false, error: home };
290
326
  }
327
+ const filled = await this.fillAt(sbx, restoredRoot, "restored");
328
+ if (this.destroyed) {
329
+ await sbx.kill().catch(() => { });
330
+ return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env destroyed during resume") };
331
+ }
332
+ if (!filled.ok) {
333
+ await this.rpc("sandbox.pause", (signal) => sbx.pause({ signal }), undefined, this.cfg.dataTimeoutMs).catch((e) => this.cfg.logger?.warn?.("remote_env_fill_refused_repause_failed", { sandboxId: sbx.sandboxId, error: errMsg(e) }));
334
+ this.cfg.metrics?.inc("remote_env_resume_total", { provider: PROVIDER, result: "error" });
335
+ if (filled.standing)
336
+ this.connectRefusal = filled.error;
337
+ return { ok: false, error: filled.error };
338
+ }
291
339
  this.sbx = sbx;
292
340
  this.paused = false;
293
341
  this.startKeepAlive();
@@ -340,9 +388,39 @@ export class RemoteContainerExecutionEnv {
340
388
  return undefined;
341
389
  const error = new RemoteExecutionError("connect_failed", verdict.refusal);
342
390
  if (verdict.answered)
343
- this.homeRefusal = error;
391
+ this.connectRefusal = error;
344
392
  return error;
345
393
  }
394
+ fillAt(sbx, root, origin) {
395
+ return this.cfg.workspaceFill === undefined ? Promise.resolve({ ok: true }) : this.cfg.workspaceFill.fill(this.fillChannel(sbx), root, origin);
396
+ }
397
+ fillChannel(sbx) {
398
+ return {
399
+ run: async (command) => {
400
+ const s = this.startIn(ADAPTER_START, command, undefined);
401
+ if (!s.ok)
402
+ return { ok: false, error: s.error.message };
403
+ try {
404
+ const r = await this.rpc("workspace.fill", (signal) => this.runIn(sbx, s.value, { signal, timeoutMs: this.cfg.dataTimeoutMs }), undefined, this.cfg.dataTimeoutMs);
405
+ return { ok: true, exitCode: r.exitCode, stdout: r.stdout, stderr: r.stderr };
406
+ }
407
+ catch (e) {
408
+ if (e instanceof CommandExitError)
409
+ return { ok: true, exitCode: e.exitCode, stdout: e.stdout, stderr: e.stderr };
410
+ return { ok: false, error: errMsg(e) };
411
+ }
412
+ },
413
+ write: async (abs, bytes) => {
414
+ try {
415
+ await this.rpc("files.write", (signal) => sbx.files.write(abs, toArrayBuffer(bytes), { signal }), undefined, this.cfg.dataTimeoutMs);
416
+ return { ok: true };
417
+ }
418
+ catch (e) {
419
+ return { ok: false, error: errMsg(e) };
420
+ }
421
+ },
422
+ };
423
+ }
346
424
  startKeepAlive() {
347
425
  this.stopKeepAlive();
348
426
  const everyMs = Math.max(30_000, Math.floor(this.cfg.timeoutMs * 0.8));
@@ -1130,10 +1208,15 @@ export class RemoteContainerExecutionEnv {
1130
1208
  }
1131
1209
  }
1132
1210
  export function e2bExecutionEnvFactory(config, deps) {
1133
- return (ctx) => new RemoteContainerExecutionEnv({
1134
- ...config,
1135
- metadata: { ...config.metadata, semaManaged: "1", sessionId: ctx.sessionId, ...(ctx.taskId ? { taskId: ctx.taskId } : {}) },
1136
- }, deps);
1211
+ const { workspaceFillOf, ...base } = config;
1212
+ return (ctx) => {
1213
+ const workspaceFill = workspaceFillOf?.(ctx);
1214
+ return new RemoteContainerExecutionEnv({
1215
+ ...base,
1216
+ metadata: { ...base.metadata, semaManaged: "1", sessionId: ctx.sessionId, ...(ctx.taskId ? { taskId: ctx.taskId } : {}) },
1217
+ ...(workspaceFill !== undefined ? { workspaceFill } : {}),
1218
+ }, deps);
1219
+ };
1137
1220
  }
1138
1221
  function errMsg(e) {
1139
1222
  return e instanceof Error ? e.message : String(e);
@@ -123,6 +123,9 @@ export interface K8sEnvConfig extends LaneHomeConfig {
123
123
  /** Run-end outcome seat — called once per `destroy()` that attempted a space snapshot (the factory's caller binds it
124
124
  * to the run, so the run's terminal frame can carry `workspaceSnapshot: "failed"`). */
125
125
  onWorkspaceSnapshot?: (outcome: "saved" | "failed") => void;
126
+ /** 恢复点的填充座(`workspace-fill.ts`;工厂按 run 装配):pod 的工作区铸出 / 恢复之后、`connect` / `resumeVM` 交还之前,经 {@link RemoteK8sExecutionEnv} 自己的
127
+ * exec 通道(不经公开面 —— 连接进行中公开面会等同一只连接记忆)往工作区里放内容。缺席 ⇒ 不放。 */
128
+ workspaceFill?: WorkspaceFill;
126
129
  /**
127
130
  * 🔴 ADDITIVE pod-spec patch (a SERVICE-side adapter seam, NOT a core seam: the K8s pod shape is
128
131
  * provider-specific, so it stays in the adapter per [ref]). A deployment supplies extra volumes / volumeMounts
@@ -171,6 +174,7 @@ import { type SandboxSecretSeatKind, type SandboxSecretVerb } from "./k8s-exec-p
171
174
  export { type PodSpecPatch, applyPodSpecPatch } from "./k8s-exec-protocol.js";
172
175
  import { type SandboxPodApi, type SandboxPodListing } from "./k8s-pod-reaper.js";
173
176
  import { type SandboxLeaseEntryInput } from "../sandbox-git-lease.js";
177
+ import type { WorkspaceFill } from "./workspace-fill.js";
174
178
  type ConnectResult = {
175
179
  ok: true;
176
180
  value: WorkspaceHandle;
@@ -318,9 +322,12 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
318
322
  private readonly inFlight;
319
323
  /** Resolvers of {@link whenIdle}: woken when the last command in flight ends. */
320
324
  private idleWaiters;
321
- /** This env's one connect in flight (see {@link connect}): the explicit connect, the lazy connect and `reconnect` that arrive while it runs
322
- * share it — one pod creation, one restore (the E2B race lesson). Cleared the moment it settles, whatever the outcome. */
325
+ /** This env's one lifecycle walk in flight — a connect ({@link connect}) or a resume ({@link resumeVM}): the explicit connect, the lazy connect of
326
+ * the first operation and `reconnect` that arrive while it runs share its answer — one pod creation, one restore, one fill (the E2B race lesson).
327
+ * Cleared the moment it settles, whatever the outcome (by the walk that set it, never by an earlier one). */
323
328
  private connectInFlight?;
329
+ /** The resume among them, with the snapshot it restores: a second `resumeVM` of the same snapshot shares it (one restore, one pod). */
330
+ private resumeInFlight?;
324
331
  /** The env's refusal — a fact of this env until `destroy`, written once ({@link holdRefusal}) and answered as it stands to every later connect (and to
325
332
  * every later pod creation: {@link provision}): the same refusal, zero pods, zero store requests, no second line. Two causes write it:
326
333
  * the space restore (`latest` names an archive that is there and did not come back — {@link restoreSnapshot}) and the pod's admission (the cluster created
@@ -355,6 +362,7 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
355
362
  private cacheUnavailableSaid;
356
363
  private readonly logger?;
357
364
  private readonly onWorkspaceSnapshot?;
365
+ private readonly workspaceFill?;
358
366
  private readonly cacheMisses?;
359
367
  constructor(config: K8sEnvConfig, deps?: K8sEnvDeps);
360
368
  /** The lease seat of the pod this env holds now — `undefined` without a broker seat or without a pod (the server's lease writes follow it: a resumed env holds a new pod and a new seat). */
@@ -391,6 +399,19 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
391
399
  connect(config?: RemoteConnectConfig): Promise<ConnectResult>;
392
400
  /** The one walk of a connect ({@link connect} runs it once at a time). */
393
401
  private establish;
402
+ /**
403
+ * The restore point's fill (`workspace-fill.ts`): a pod whose workspace this env just minted or restored is handed over only once the fill seat placed
404
+ * its content — through {@link fillChannel} (this adapter's own exec, never the public face: inside a connect the public face waits on that very connect).
405
+ * A fill that does not complete releases the pod (no tool and no run-end snapshot may see a half-filled workspace); a refusal that would answer the same
406
+ * again (`standing`) is held for the env ({@link holdRefusal}). No seat ⇒ the pod as it came. `origin` tells the seat whether the workspace was minted
407
+ * (a new pod with nothing restored: the space off / unavailable / without `latest`, or `latest` dangling) or restored (the space's `latest`, or a park snapshot whose archive came back; a park snapshot with no archive — a plan-only park — is minted).
408
+ */
409
+ private settle;
410
+ /** The restore point's channel: a fixed command through the one start seam (start form `adapter`, the engine's commands' identity) and a write in
411
+ * {@link writeChunked}'s form — both straight on {@link runExec} with the data bound (never {@link ensureConnected}). */
412
+ private fillChannel;
413
+ /** Release the pod this env holds (its workspace is not handed over): the pod deleted, its seats cleared. No pod ⇒ nothing. */
414
+ private releasePod;
394
415
  /** This env holds a workspace its `destroy()` will commit ⇒ arm its seat on the space's commit fence (from here the
395
416
  * run's terminal write fences the space; `workspace-space-commit.ts`). Each write of this run's row sets its deadline to
396
417
  * now + the upload's own wall-clock bound (`dataTimeoutMs`). No-op unless the space lane is open. */
@@ -632,7 +653,9 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
632
653
  /**
633
654
  * Resume a workspace snapshot onto a FRESH pod (resume = executionEnvFactory rebuild + resumeVM):
634
655
  * provision a pod, then download + untar the snapshot into the workspace ({@link restoreSnapshot}, lane `park`). A
635
- * missing object (empty/plan-only snapshot) resumes as a clean workspace.
656
+ * missing object (a plan-only park: no pod was ever minted, so nothing was uploaded) resumes as a clean workspace whose origin is `minted` —
657
+ * no restorable tree — so the restore point seeds it; the resume walks the park lane and never reads the space's `latest` (which a fresh connect with the space on
658
+ * would restore); a snapshot that came back is `restored` and never seeded.
636
659
  *
637
660
  * [ref]①(priorHandle 根保真)——k8s 走**诚实臂**而非绑定臂:pod 的 workspace volume 挂在**当前**
638
661
  * 配置根(cfg.mountPath),往 priorHandle.mountPath 绑=往容器 rootfs untar 大工作区,ephemeral-storage
@@ -641,10 +664,13 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
641
664
  * 变更窗的分歧由 core 侧 fail-closed reopen 兜底,审批不烧错根。
642
665
  */
643
666
  resumeVM(snapshotId: SnapshotId, options?: VmLifecycleOptions): Promise<ConnectResult>;
667
+ /** The one walk of a resume ({@link resumeVM} runs it under the connect gate). */
668
+ private resumeWalk;
644
669
  /**
645
670
  * The ONE tar download (both lanes): provision a FRESH pod, download + untar the lane's archive into the workspace.
646
- * A missing archive (404) starts from the clean workspace on both lanes — what differs is what it means:
647
- * - park: a plan-only suspend has no object — nothing to say;
671
+ * A missing archive (404) starts from the clean workspace on both lanes, and on both it is the same fact — no restorable tree ⇒ origin
672
+ * `minted` (the restore point seeds it, exactly as on a fresh connect). What differs is only what it means:
673
+ * - park: a plan-only suspend has no object (the park came before anything touched the hand) — nothing to say;
648
674
  * - space: `latest` dangles (its pointer is only written after a successful upload, so the archive was removed later —
649
675
  * by the bucket's lifecycle rule or a hand): the data is gone by an operator rule ⇒ loud and continuable —
650
676
  * {@link dropDanglingLatest} (one `workspace_latest_dangling` line, the pointer deleted); the run's own run-end
@@ -48,6 +48,9 @@ export { applyPodSpecPatch } from "./k8s-exec-protocol.js";
48
48
  import { sandboxRunEndHoldMs } from "./k8s-pod-reaper.js";
49
49
  import { BROKER_COMMAND, BROKER_CONTAINER_NAME, BROKER_LEASE_DIR, BROKER_LEASE_KEY, BROKER_PROBE_ARG, BROKER_VOLUME_NAME } from "../broker/constants.js";
50
50
  import { buildSandboxLeaseFile, placeholderLeaseEntries, sandboxGitRouteEnv } from "../sandbox-git-lease.js";
51
+ function minted(c) {
52
+ return c.ok ? { ...c, origin: "minted" } : c;
53
+ }
51
54
  const CACHE_VOLUME = ADAPTER_VOLUME_NAMES.cache;
52
55
  function occupantKeyOf(occupant) {
53
56
  if (occupant === undefined)
@@ -213,6 +216,7 @@ export class RemoteK8sExecutionEnv {
213
216
  inFlight = new Set();
214
217
  idleWaiters = [];
215
218
  connectInFlight;
219
+ resumeInFlight;
216
220
  connectRefusal;
217
221
  get homeDir() {
218
222
  return this.homeSeat.home;
@@ -231,6 +235,7 @@ export class RemoteK8sExecutionEnv {
231
235
  cacheUnavailableSaid = false;
232
236
  logger;
233
237
  onWorkspaceSnapshot;
238
+ workspaceFill;
234
239
  cacheMisses;
235
240
  constructor(config, deps) {
236
241
  this.cfg = {
@@ -270,6 +275,7 @@ export class RemoteK8sExecutionEnv {
270
275
  this.cacheLane = cacheLaneOf(config.cache, config.spaceOccupant);
271
276
  this.logger = config.logger;
272
277
  this.onWorkspaceSnapshot = config.onWorkspaceSnapshot;
278
+ this.workspaceFill = config.workspaceFill;
273
279
  this.cacheMisses = config.cacheMisses;
274
280
  this.floor = this.deps.readFloor ?? createSandboxReadFloor({ lane: PROVIDER, knob: config.sandboxReadFloor, ...(config.logger !== undefined ? { logger: config.logger } : {}) });
275
281
  this.capabilities = capabilitiesFor({ remoteExec: { provider: PROVIDER, s3Snapshot: this.cfg.s3Snapshot }, sandboxReadFloor: config.sandboxReadFloor });
@@ -374,10 +380,14 @@ export class RemoteK8sExecutionEnv {
374
380
  });
375
381
  }
376
382
  connect(config) {
377
- this.connectInFlight ??= this.establish(config).finally(() => {
378
- this.connectInFlight = undefined;
383
+ if (this.connectInFlight)
384
+ return this.connectInFlight;
385
+ const walk = this.establish(config).finally(() => {
386
+ if (this.connectInFlight === walk)
387
+ this.connectInFlight = undefined;
379
388
  });
380
- return this.connectInFlight;
389
+ this.connectInFlight = walk;
390
+ return walk;
381
391
  }
382
392
  async establish(config) {
383
393
  if (this.connectRefusal)
@@ -386,10 +396,10 @@ export class RemoteK8sExecutionEnv {
386
396
  const lane = this.spaceLane;
387
397
  switch (lane.state) {
388
398
  case "off":
389
- return this.provision(signal);
399
+ return this.settle(minted(await this.provision(signal)));
390
400
  case "unavailable":
391
401
  this.sayUnavailable(lane.reason);
392
- return this.provision(signal);
402
+ return this.settle(minted(await this.provision(signal)));
393
403
  case "unreadable":
394
404
  return this.provision(signal);
395
405
  case "open": {
@@ -399,7 +409,7 @@ export class RemoteK8sExecutionEnv {
399
409
  const latest = await this.readLatest(lane.space, signal);
400
410
  if (!latest.ok)
401
411
  return latest;
402
- const c = latest.value === undefined ? await this.provision(signal) : await this.restoreSnapshot({ lane: "space", space: lane.space }, latest.value, signal);
412
+ const c = await this.settle(latest.value === undefined ? minted(await this.provision(signal)) : await this.restoreSnapshot({ lane: "space", space: lane.space }, latest.value, signal));
403
413
  if (c.ok)
404
414
  this.armSpace();
405
415
  return c;
@@ -410,6 +420,43 @@ export class RemoteK8sExecutionEnv {
410
420
  }
411
421
  }
412
422
  }
423
+ async settle(c) {
424
+ if (!c.ok)
425
+ return c;
426
+ const fill = this.workspaceFill;
427
+ if (fill !== undefined) {
428
+ const filled = await fill.fill(this.fillChannel(), this.cwd, c.origin);
429
+ if (!filled.ok) {
430
+ await this.releasePod();
431
+ return filled.standing ? this.holdRefusal(filled.error) : { ok: false, error: filled.error };
432
+ }
433
+ }
434
+ if (this.destroyed) {
435
+ await this.releasePod();
436
+ return { ok: false, error: new RemoteExecutionError("connect_failed", "env destroyed during connect") };
437
+ }
438
+ return { ok: true, value: c.value };
439
+ }
440
+ fillChannel() {
441
+ const exec = (command) => this.runExec(ADAPTER_START, command, { capMs: this.cfg.dataTimeoutMs });
442
+ return {
443
+ run: async (command) => {
444
+ const r = await exec(command);
445
+ return r.ok ? { ok: true, exitCode: r.value.exitCode, stdout: r.value.stdout, stderr: r.value.stderr } : { ok: false, error: r.error.message };
446
+ },
447
+ write: async (abs, bytes) => {
448
+ const w = await this.writeChunked(abs, bytes, true, undefined, exec);
449
+ return w.ok ? { ok: true } : { ok: false, error: w.error.message };
450
+ },
451
+ };
452
+ }
453
+ async releasePod() {
454
+ const name = this.podName;
455
+ this.podName = undefined;
456
+ this.podIP = undefined;
457
+ if (name)
458
+ await this.deletePod(name).catch(() => { });
459
+ }
413
460
  armSpace() {
414
461
  if (this.spaceLane.state === "open")
415
462
  this.spaceLane.space.fence.arm(this.cfg.dataTimeoutMs);
@@ -1202,10 +1249,31 @@ export class RemoteK8sExecutionEnv {
1202
1249
  await this.deletePod(name).catch(() => { });
1203
1250
  return ok(snapshotId);
1204
1251
  }
1205
- async resumeVM(snapshotId, options) {
1252
+ resumeVM(snapshotId, options) {
1206
1253
  if (!this.cfg.s3Snapshot)
1207
- return unsupported("resumeVM");
1208
- const r = await this.restoreSnapshot({ lane: "park" }, snapshotId, options?.abortSignal);
1254
+ return Promise.resolve(unsupported("resumeVM"));
1255
+ const same = this.resumeInFlight;
1256
+ if (same?.snapshotId === snapshotId)
1257
+ return same.walk;
1258
+ const prior = this.connectInFlight;
1259
+ const walk = this.resumeWalk(prior, snapshotId, options).finally(() => {
1260
+ if (this.connectInFlight === walk)
1261
+ this.connectInFlight = undefined;
1262
+ if (this.resumeInFlight?.walk === walk)
1263
+ this.resumeInFlight = undefined;
1264
+ });
1265
+ this.connectInFlight = walk;
1266
+ this.resumeInFlight = { snapshotId, walk };
1267
+ return walk;
1268
+ }
1269
+ async resumeWalk(prior, snapshotId, options) {
1270
+ if (prior)
1271
+ await prior;
1272
+ if (this.podName) {
1273
+ this.logger?.warn("sandbox_pod_released_before_restore", { pod: this.podName, ...(this.cfg.sessionId ? { sessionId: this.cfg.sessionId } : {}), effect: "this env held a pod minted before the park snapshot's restore — it is deleted; the restore provisions the leg's one pod" });
1274
+ await this.releasePod();
1275
+ }
1276
+ const r = await this.settle(await this.restoreSnapshot({ lane: "park" }, snapshotId, options?.abortSignal));
1209
1277
  if (!r.ok)
1210
1278
  return r;
1211
1279
  this.handleSnapshotId = snapshotId;
@@ -1234,15 +1302,14 @@ export class RemoteK8sExecutionEnv {
1234
1302
  ? { cls: restoreClassOfExit(r.value.exitCode), detail: `workspace snapshot restore exited ${r.value.exitCode}: ${this.redactUrl(r.value.stderr, url).slice(0, 300)}` }
1235
1303
  : undefined;
1236
1304
  if (!failure) {
1237
- if (target.lane === "space" && r.ok && r.value.stdout.includes("EMPTY_SNAPSHOT"))
1238
- await this.dropDanglingLatest(target.space, snapshotId);
1239
- return ok(this.workspaceHandle());
1305
+ if (r.ok && r.value.stdout.includes("EMPTY_SNAPSHOT")) {
1306
+ if (target.lane === "space")
1307
+ await this.dropDanglingLatest(target.space, snapshotId);
1308
+ return { ok: true, value: this.workspaceHandle(), origin: "minted" };
1309
+ }
1310
+ return { ok: true, value: this.workspaceHandle(), origin: "restored" };
1240
1311
  }
1241
- const name = this.podName;
1242
- this.podName = undefined;
1243
- this.podIP = undefined;
1244
- if (name)
1245
- await this.deletePod(name).catch(() => { });
1312
+ await this.releasePod();
1246
1313
  if (attempt < RESTORE_MAX_ATTEMPTS[target.lane] && restoreFailureIsTransient(failure.cls)) {
1247
1314
  if (await this.restorePause(abortSignal))
1248
1315
  continue;
@@ -2081,12 +2148,12 @@ export class RemoteK8sExecutionEnv {
2081
2148
  const abs = this.resolve(p);
2082
2149
  return appendFileStaged(this.stagedLane(content), abs, abortSignal, (ctx) => ctx.inPlace((signal) => this.writeChunked(abs, content, false, signal)));
2083
2150
  }
2084
- async writeChunked(abs, content, truncateFirst, abortSignal) {
2151
+ async writeChunked(abs, content, truncateFirst, abortSignal, exec = (command) => this.execData(command, abortSignal)) {
2085
2152
  const bytes = typeof content === "string" ? Buffer.from(content, "utf8") : Buffer.from(content);
2086
2153
  for (let off = 0, first = true; off < bytes.length || first; off += WRITE_CHUNK_BYTES, first = false) {
2087
2154
  const b64 = bytes.subarray(off, off + WRITE_CHUNK_BYTES).toString("base64");
2088
2155
  const redirect = first && truncateFirst ? ">" : ">>";
2089
- const r = await this.execData(`printf '%s' ${shellQuote(b64)} | base64 -d ${redirect} ${shellQuote(abs)}`, abortSignal);
2156
+ const r = await exec(`printf '%s' ${shellQuote(b64)} | base64 -d ${redirect} ${shellQuote(abs)}`);
2090
2157
  if (!r.ok)
2091
2158
  return { ok: false, error: fileErrorFromExec(r.error, abs) };
2092
2159
  if (r.value.exitCode !== 0)
@@ -3,6 +3,7 @@ import { type LaneHomeConfig } from "../lane-home-probe.js";
3
3
  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 ExecutionEnvCapabilities } from "@sema-agent/core";
4
4
  import { type SandboxReadFloorKnob } from "../execution-lane-caps.js";
5
5
  import { type SandboxReadFloor } from "./sandbox-read-floor.js";
6
+ import type { WorkspaceFill, WorkspaceFillOf } from "./workspace-fill.js";
6
7
  /** Options accepted by `ExecutionEnv.exec` (core does not re-export the type; derive it from the seam). */
7
8
  type ExecOpts = Parameters<ExecutionEnv["exec"]>[1];
8
9
  /** Construction config for {@link RemoteLocalDockerExecutionEnv}. */
@@ -59,6 +60,8 @@ export interface LocalDockerEnvConfig extends LaneHomeConfig {
59
60
  id?: string;
60
61
  /** `SANDBOX_READ_FLOOR`(keep | off;缺席 ⇒ keep):本容器守不守读拒表的字节地板 = 读地板模块服务本车道 ∧ 本键不是 off(`sandbox-read-floor.ts`)。 */
61
62
  sandboxReadFloor?: SandboxReadFloorKnob;
63
+ /** 恢复点的填充座(`workspace-fill.ts`;工厂按 run 装配):容器新建之后、交给本环境之前,经本适配器自己的 `docker exec` 往工作区里放内容。缺席 ⇒ 不放。 */
64
+ workspaceFill?: WorkspaceFill;
62
65
  }
63
66
  /** Inject `spawn` for tests (a fake docker CLI). Defaults to node's child_process spawn. */
64
67
  export interface LocalDockerEnvDeps {
@@ -92,12 +95,15 @@ export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv
92
95
  private destroyed;
93
96
  /** In-flight connect, memoized so concurrent lazy first-use starts exactly one container (no double-run). */
94
97
  private connecting?;
95
- /** 家目录:环境答了而与声明不一致 ⇒ 这只环境的拒,记一次、此后每次连接原样回它(同一镜像同一答案:不再起容器、不再问)。 */
96
- private homeRefusal?;
98
+ /** 这只环境的拒 —— 记一次、此后每次连接原样回它(同一镜像同一答案:不再起容器、不再问)。两个写者:家目录(环境答了而与声明不一致)
99
+ * 与恢复点的填充(换一只容器也是同一个答的拒:种子不是项目包、摘要不符、镜像没有 `tar`、镜像预置撞名 —— `FillOutcome.standing`)。 */
100
+ private connectRefusal?;
97
101
  /** 家目录的座(声明在场 ⇒ 开环境那一拍对证 · 否则开容器那一拍实测 · 实测缺席 ⇒ 缺席 + 一行 warn)。与其余读得出家目录的车道同一只判官、同一句拒。 */
98
102
  private readonly homeSeat;
99
103
  constructor(config: LocalDockerEnvConfig, deps?: LocalDockerEnvDeps);
100
104
  private resolve;
105
+ /** 收据句柄。还没连上(懒环境:续跑腿的引擎在恢复相**之前**先取它比车道)⇒ 未连接形 `{ sandboxId: "(unconnected)", provider, mountPath }`(与 k8s / e2b 车道同形),
106
+ * 不抛 —— 抛了,续跑腿在恢复相就失败。 */
101
107
  workspaceHandle(): WorkspaceHandle;
102
108
  connect(config?: RemoteConnectConfig): Promise<{
103
109
  ok: true;
@@ -107,6 +113,10 @@ export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv
107
113
  error: RemoteExecutionError;
108
114
  }>;
109
115
  private doConnect;
116
+ /** 恢复点的填充:有座 ⇒ 经 {@link fillChannel} 放(容器恒是新起的 ⇒ `minted`;本车道不可挂起);没有 ⇒ 什么都不放。 */
117
+ private fillWorkspace;
118
+ /** 恢复点手里的通道:`docker exec` 经唯一起动接缝(起动形 `adapter`,与引擎命令同一执行用户;连接进行中,不经懒连接的文件面);写 = 容器内 `cat >` 从 stdin 收整份字节。 */
119
+ private fillChannel;
110
120
  /**
111
121
  * 家目录:座要问({@link LaneHomeSeat.asks}:声明在场 ⇒ 对证;没声明 ⇒ 实测)⇒ 在本容器里问一次 `$HOME`(与命令同一条 `docker exec` 通道、同一份 `-e` env),读数交座。
112
122
  * 声明在场:环境答了而不一致 / 不是绝对路径 ⇒ 回拒并记在环境上(同一镜像同一答案:此后每次连接原样回它,不再起容器、不再问);没答上(exec 失败 / 超时 / 非零退出)⇒ 这一次同样不开,
@@ -258,6 +268,8 @@ export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv
258
268
  * owns the lifetime and calls `destroy()` (= `docker rm -f`) on task end. Folds the per-task `ctx.taskId`/
259
269
  * `ctx.sessionId` into the container name so two concurrent tasks never share a container.
260
270
  */
261
- export declare function localDockerExecutionEnvFactory(config: LocalDockerEnvConfig, deps?: LocalDockerEnvDeps): ExecutionEnvFactory;
271
+ export declare function localDockerExecutionEnvFactory(config: Omit<LocalDockerEnvConfig, "workspaceFill"> & {
272
+ readonly workspaceFillOf?: WorkspaceFillOf;
273
+ }, deps?: LocalDockerEnvDeps): ExecutionEnvFactory;
262
274
  export {};
263
275
  //# sourceMappingURL=remote-env-local-docker.d.ts.map
@@ -37,7 +37,7 @@ export class RemoteLocalDockerExecutionEnv {
37
37
  handle;
38
38
  destroyed = false;
39
39
  connecting;
40
- homeRefusal;
40
+ connectRefusal;
41
41
  homeSeat;
42
42
  constructor(config, deps = {}) {
43
43
  if (!config.image || !config.image.trim()) {
@@ -64,17 +64,15 @@ export class RemoteLocalDockerExecutionEnv {
64
64
  return path.posix.isAbsolute(p) ? path.posix.normalize(p) : path.posix.normalize(path.posix.join(this.cwd, p));
65
65
  }
66
66
  workspaceHandle() {
67
- if (!this.handle)
68
- throw new RemoteExecutionError("connect_failed", "workspaceHandle() called before connect()");
69
- return this.handle;
67
+ return this.handle ?? { sandboxId: "(unconnected)", provider: PROVIDER, mountPath: this.cfg.mountPath };
70
68
  }
71
69
  async connect(config) {
72
70
  if (config?.snapshotId)
73
71
  return unsupported("connect(snapshotId)");
74
72
  if (this.destroyed)
75
73
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env already destroyed") };
76
- if (this.homeRefusal)
77
- return { ok: false, error: this.homeRefusal };
74
+ if (this.connectRefusal)
75
+ return { ok: false, error: this.connectRefusal };
78
76
  if (config?.abortSignal?.aborted)
79
77
  return { ok: false, error: new RemoteExecutionError("aborted", "connect aborted") };
80
78
  if (this.containerId)
@@ -139,12 +137,38 @@ export class RemoteLocalDockerExecutionEnv {
139
137
  await this.forceRemove().catch(() => { });
140
138
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env destroyed during connect") };
141
139
  }
140
+ const filled = await this.fillWorkspace();
141
+ if (!filled.ok || this.destroyed) {
142
+ await this.forceRemove().catch(() => { });
143
+ if (!filled.ok && filled.standing)
144
+ this.connectRefusal = filled.error;
145
+ return { ok: false, error: filled.ok ? new RemoteExecutionError("connect_failed", "execution env destroyed during connect") : filled.error };
146
+ }
142
147
  this.containerId = run.value.stdout.trim() || this.containerName;
143
148
  this.handle = { sandboxId: this.containerName, provider: PROVIDER, mountPath: this.cfg.mountPath, sessionToken: this.containerName };
144
149
  if (config?.secrets?.length) {
145
150
  }
146
151
  return ok(this.handle);
147
152
  }
153
+ fillWorkspace() {
154
+ return this.cfg.workspaceFill === undefined ? Promise.resolve({ ok: true }) : this.cfg.workspaceFill.fill(this.fillChannel(), this.cfg.mountPath, "minted");
155
+ }
156
+ fillChannel() {
157
+ const run = async (command, input) => {
158
+ const argv = this.startArgv(ADAPTER_START, command, { shell: "/bin/sh", ...(input !== undefined ? { stdin: true } : {}) });
159
+ if (!argv.ok)
160
+ return { ok: false, error: argv.error.message };
161
+ const r = await this.docker(argv.value, { timeoutMs: this.cfg.commandTimeoutMs, ...(input !== undefined ? { input } : {}) });
162
+ return r.ok ? { ok: true, exitCode: r.value.exitCode, stdout: r.value.stdout, stderr: r.value.stderr } : { ok: false, error: r.error.message };
163
+ };
164
+ return {
165
+ run: (command) => run(command),
166
+ write: async (abs, bytes) => {
167
+ const r = await run(`cat > ${shellQuote(abs)}`, bytes);
168
+ return !r.ok ? r : r.exitCode === 0 ? { ok: true } : { ok: false, error: r.stderr.trim() || `exit ${r.exitCode}` };
169
+ },
170
+ };
171
+ }
148
172
  async homeVerdict(signal) {
149
173
  if (!this.homeSeat.asks)
150
174
  return undefined;
@@ -157,7 +181,7 @@ export class RemoteLocalDockerExecutionEnv {
157
181
  return undefined;
158
182
  const error = new RemoteExecutionError("connect_failed", verdict.refusal);
159
183
  if (verdict.answered)
160
- this.homeRefusal = error;
184
+ this.connectRefusal = error;
161
185
  return error;
162
186
  }
163
187
  async suspendVM(_options) {
@@ -172,8 +196,8 @@ export class RemoteLocalDockerExecutionEnv {
172
196
  async reconnect(sessionToken) {
173
197
  if (this.destroyed)
174
198
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env already destroyed") };
175
- if (this.homeRefusal)
176
- return { ok: false, error: this.homeRefusal };
199
+ if (this.connectRefusal)
200
+ return { ok: false, error: this.connectRefusal };
177
201
  if (sessionToken && sessionToken !== this.containerName) {
178
202
  return { ok: false, error: new RemoteExecutionError("connect_failed", `reconnect token '${String(sessionToken)}' does not match this env's container '${this.containerName}'`) };
179
203
  }
@@ -189,8 +213,8 @@ export class RemoteLocalDockerExecutionEnv {
189
213
  const home = this.containerId === undefined ? await this.homeVerdict() : undefined;
190
214
  if (home !== undefined)
191
215
  return { ok: false, error: home };
192
- if (this.destroyed || this.homeRefusal)
193
- return { ok: false, error: this.homeRefusal ?? new RemoteExecutionError("connect_failed", "execution env destroyed during reconnect") };
216
+ if (this.destroyed || this.connectRefusal)
217
+ return { ok: false, error: this.connectRefusal ?? new RemoteExecutionError("connect_failed", "execution env destroyed during reconnect") };
194
218
  this.containerId = this.containerName;
195
219
  this.handle ??= { sandboxId: this.containerName, provider: PROVIDER, mountPath: this.cfg.mountPath, sessionToken: this.containerName };
196
220
  return ok(this.handle);
@@ -682,6 +706,10 @@ function errMsg(e) {
682
706
  return e instanceof Error ? e.message : String(e);
683
707
  }
684
708
  export function localDockerExecutionEnvFactory(config, deps = {}) {
685
- return (ctx) => new RemoteLocalDockerExecutionEnv({ ...config, id: config.id ?? ctx.taskId ?? ctx.sessionId }, deps);
709
+ const { workspaceFillOf, ...base } = config;
710
+ return (ctx) => {
711
+ const workspaceFill = workspaceFillOf?.(ctx);
712
+ return new RemoteLocalDockerExecutionEnv({ ...base, id: base.id ?? ctx.taskId ?? ctx.sessionId, ...(workspaceFill !== undefined ? { workspaceFill } : {}) }, deps);
713
+ };
686
714
  }
687
715
  //# sourceMappingURL=remote-env-local-docker.js.map