@sema-agent/server 7.128.0 → 7.129.1

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 (105) hide show
  1. package/MIGRATION.md +140 -0
  2. package/USAGE.md +114 -18
  3. package/dist/approval-card.d.ts +53 -26
  4. package/dist/approval-card.js +40 -11
  5. package/dist/approval-crash-convergence.js +4 -4
  6. package/dist/bake-runner/main.js +2 -1
  7. package/dist/boot/execution-env.js +1 -0
  8. package/dist/boot/memory-consolidation.d.ts +46 -16
  9. package/dist/boot/memory-consolidation.js +15 -8
  10. package/dist/boot/side-query-lane.d.ts +7 -20
  11. package/dist/boot/side-query-lane.js +0 -24
  12. package/dist/boot/stage-01-config.js +3 -0
  13. package/dist/boot/stage-06-runners.js +1 -1
  14. package/dist/boot/stores.js +2 -2
  15. package/dist/boot/web-admin.js +17 -10
  16. package/dist/boot/webfetch-summarize-lane.d.ts +10 -7
  17. package/dist/boot/webfetch-summarize-lane.js +2 -3
  18. package/dist/brain.js +6 -9
  19. package/dist/capabilities/builtin-tools.d.ts +2 -2
  20. package/dist/capabilities/engine-capabilities.d.ts +14 -0
  21. package/dist/capabilities/engine-capabilities.js +12 -0
  22. package/dist/capability-facts.d.ts +3 -1
  23. package/dist/capability-facts.js +1 -0
  24. package/dist/config-catalog.js +5 -1
  25. package/dist/config-types.d.ts +7 -3
  26. package/dist/config.js +7 -1
  27. package/dist/egress-redirect.d.ts +12 -0
  28. package/dist/egress-redirect.js +7 -0
  29. package/dist/entry-mode-clamp-notice.d.ts +30 -0
  30. package/dist/entry-mode-clamp-notice.js +13 -0
  31. package/dist/exec-bounds.d.ts +69 -0
  32. package/dist/exec-bounds.js +37 -0
  33. package/dist/fleet/fleet-bus.d.ts +7 -0
  34. package/dist/fleet/fleet-bus.js +1 -0
  35. package/dist/hooks/hook-llm.js +6 -2
  36. package/dist/host-lsp-manager.d.ts +3 -3
  37. package/dist/http/admission.js +5 -2
  38. package/dist/http/resume-legs.js +46 -27
  39. package/dist/http/route-ctx.d.ts +1 -1
  40. package/dist/http/routes/approvals-assistant.js +6 -4
  41. package/dist/http/routes/capabilities.js +1 -0
  42. package/dist/http/routes/session-sync.js +3 -4
  43. package/dist/http/routes/sessions.js +4 -17
  44. package/dist/http/send.d.ts +1 -1
  45. package/dist/http/send.js +4 -3
  46. package/dist/http/wire-types.d.ts +3 -2
  47. package/dist/index.d.ts +1 -1
  48. package/dist/index.js +1 -1
  49. package/dist/observability/fail-open.d.ts +7 -3
  50. package/dist/observability/fail-open.js +7 -3
  51. package/dist/observability/secret-env-scrub.d.ts +37 -3
  52. package/dist/observability/secret-env-scrub.js +2 -2
  53. package/dist/parked-decide.js +5 -2
  54. package/dist/plugins/memory-embedder.js +5 -0
  55. package/dist/plugins/one-time-migrations.d.ts +180 -9
  56. package/dist/plugins/one-time-migrations.js +315 -3
  57. package/dist/plugins/pg-pool.js +3 -1
  58. package/dist/plugins/remote-env-e2b.d.ts +31 -9
  59. package/dist/plugins/remote-env-e2b.js +77 -26
  60. package/dist/plugins/remote-env-host.d.ts +17 -20
  61. package/dist/plugins/remote-env-host.js +20 -30
  62. package/dist/plugins/remote-env-k8s.d.ts +12 -4
  63. package/dist/plugins/remote-env-k8s.js +22 -31
  64. package/dist/plugins/session-policy-store-sql.d.ts +16 -4
  65. package/dist/plugins/session-policy-store-sql.js +17 -20
  66. package/dist/plugins/session-rules-column.d.ts +16 -0
  67. package/dist/plugins/session-rules-column.js +6 -0
  68. package/dist/plugins/tidb-pool.js +4 -1
  69. package/dist/read-face-posture.d.ts +2 -1
  70. package/dist/request-value-closure.js +1 -1
  71. package/dist/retired-card-words.d.ts +17 -0
  72. package/dist/retired-card-words.js +49 -0
  73. package/dist/rule-entry-list.js +2 -0
  74. package/dist/run-local.js +10 -3
  75. package/dist/server-secret-env.d.ts +77 -85
  76. package/dist/server-secret-env.js +40 -26
  77. package/dist/session-policy-wire.d.ts +61 -124
  78. package/dist/session-policy-wire.js +59 -121
  79. package/dist/session-sync.d.ts +8 -6
  80. package/dist/session-sync.js +6 -5
  81. package/dist/spec-fields.d.ts +3 -2
  82. package/dist/standing-answer-grant.d.ts +17 -9
  83. package/dist/standing-answer-grant.js +11 -8
  84. package/dist/task-settings.d.ts +6 -1
  85. package/dist/task-settings.js +1 -1
  86. package/dist/tool-approval.d.ts +14 -7
  87. package/dist/tool-approval.js +23 -18
  88. package/dist/trace/core-keyset-guard.d.ts +3 -3
  89. package/dist/trace/engine-notice-wire.d.ts +1 -1
  90. package/dist/trace/engine-notice-wire.js +1 -0
  91. package/dist/trace/project.d.ts +7 -4
  92. package/dist/trace/project.js +6 -75
  93. package/dist/trace/tool-output-media.d.ts +2 -1
  94. package/dist/trace/tool-output-media.js +3 -25
  95. package/dist/web-admin-client/apply-effective.js +28 -11
  96. package/dist/web-admin-client/facade.d.ts +1 -1
  97. package/dist/web-admin-client/facade.js +1 -1
  98. package/dist/web-admin-client/read-face.d.ts +73 -20
  99. package/dist/web-admin-client/read-face.js +76 -36
  100. package/dist/web-admin-client/skills-mcp.d.ts +1 -1
  101. package/dist/web-admin-client/skills-mcp.js +1 -1
  102. package/dist/web-admin-client/types.d.ts +3 -3
  103. package/package.json +3 -3
  104. package/dist/mcp-tool-axes.d.ts +0 -62
  105. package/dist/mcp-tool-axes.js +0 -47
@@ -3,13 +3,13 @@ import { Sandbox, CommandExitError, FileType } from "e2b";
3
3
  import { FileError, ExecutionError, RemoteExecutionError, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
4
4
  import { fileErrorFromExec } from "./remote-env-file-error.js";
5
5
  import { numEnvOr, runSymlinkChain, shellQuote } from "./remote-shell.js";
6
+ import { commandBoundsOf, firstBoundOf, laneLivenessOf, livenessCutMessage, LIVENESS_MS_DEFAULT } from "../exec-bounds.js";
6
7
  import { BackgroundShellManager } from "./background-shell-support.js";
7
8
  const PROVIDER = "e2b";
8
9
  const DEFAULT_MOUNT_PATH = "/home/user";
9
10
  const DEFAULT_TIMEOUT_MS = 5 * 60_000;
10
11
  const E2B_SANDBOX_MAX_MS = numEnvOr("E2B_SANDBOX_MAX_MS", 3_600_000, 60_000);
11
12
  const DEFAULT_RPC_TIMEOUT_MS = 60_000;
12
- const DEFAULT_LIVENESS_MS = 120_000;
13
13
  const DEFAULT_DATA_TIMEOUT_MS = 5 * 60_000;
14
14
  const E2B_BG_MAX_CONCURRENT = numEnvOr("E2B_BG_MAX_CONCURRENT", 8, 1);
15
15
  const E2B_BG_DEFAULT_TIMEOUT_SEC = numEnvOr("E2B_BG_DEFAULT_TIMEOUT_SEC", 300, 1);
@@ -22,6 +22,7 @@ export class RemoteContainerExecutionEnv {
22
22
  cdPath = "unknown";
23
23
  cfg;
24
24
  sandboxApi;
25
+ laneLiveness;
25
26
  sbx;
26
27
  keepAlive;
27
28
  handle;
@@ -36,9 +37,10 @@ export class RemoteContainerExecutionEnv {
36
37
  timeoutMs: Math.min(config.timeoutMs ?? DEFAULT_TIMEOUT_MS, E2B_SANDBOX_MAX_MS),
37
38
  allowInternetAccess: config.allowInternetAccess ?? true,
38
39
  rpcTimeoutMs: config.rpcTimeoutMs ?? DEFAULT_RPC_TIMEOUT_MS,
39
- livenessMs: config.livenessMs ?? DEFAULT_LIVENESS_MS,
40
+ livenessMs: config.livenessMs ?? LIVENESS_MS_DEFAULT,
40
41
  dataTimeoutMs: config.dataTimeoutMs ?? DEFAULT_DATA_TIMEOUT_MS,
41
42
  };
43
+ this.laneLiveness = laneLivenessOf(this.cfg.livenessMs, "E2B_LIVENESS_MS");
42
44
  this.cwd = this.cfg.mountPath;
43
45
  this.homeDir = config.homeDir;
44
46
  this.sandboxApi = deps.sandboxApi ?? Sandbox;
@@ -255,9 +257,14 @@ export class RemoteContainerExecutionEnv {
255
257
  s.total += b.length;
256
258
  };
257
259
  let lastProgress = Date.now();
260
+ const startedAt = lastProgress;
261
+ const bounds = commandBoundsOf({ timeoutMs: options?.timeout != null ? options.timeout * 1000 : undefined }, this.laneLiveness);
262
+ const first = firstBoundOf(bounds);
258
263
  let handle;
264
+ let starting;
265
+ let startCut = false;
259
266
  try {
260
- handle = await this.rpc("commands.run", () => sbx.commands.run(command, {
267
+ handle = await this.rpc("commands.run", () => (starting = sbx.commands.run(command, {
261
268
  background: true,
262
269
  cwd: this.resolve(options?.cwd ?? this.cwd),
263
270
  ...(options?.env ? { envs: options.env } : {}),
@@ -265,6 +272,8 @@ export class RemoteContainerExecutionEnv {
265
272
  ? Math.max(this.commandTimeoutMs(options?.timeout), (E2B_BG_MAX_TIMEOUT_SEC + E2B_BG_PROVIDER_BACKSTOP_PAD_SEC) * 1000)
266
273
  : this.commandTimeoutMs(options?.timeout),
267
274
  onStdout: (d) => {
275
+ if (startCut)
276
+ return;
268
277
  if (tee)
269
278
  teePush(tee.stdout, d);
270
279
  if (!detached) {
@@ -273,6 +282,8 @@ export class RemoteContainerExecutionEnv {
273
282
  }
274
283
  },
275
284
  onStderr: (d) => {
285
+ if (startCut)
286
+ return;
276
287
  if (tee)
277
288
  teePush(tee.stderr, d);
278
289
  if (!detached) {
@@ -280,11 +291,13 @@ export class RemoteContainerExecutionEnv {
280
291
  options?.onStderr?.(d);
281
292
  }
282
293
  },
283
- }), options?.abortSignal, this.cfg.livenessMs);
294
+ })), options?.abortSignal, first.ms);
284
295
  }
285
296
  catch (e) {
286
- this.activeCommands--;
287
- return { ok: false, error: this.execError(e, options?.abortSignal) };
297
+ startCut = true;
298
+ this.settleAbandonedStart(starting);
299
+ const cut = e instanceof RemoteExecutionError && e.code === "timeout" && !options?.abortSignal?.aborted;
300
+ return { ok: false, error: cut ? new ExecutionError("timeout", boundCutMessage(first)) : this.execError(e, options?.abortSignal) };
288
301
  }
289
302
  const onAbort = () => { if (!detached)
290
303
  void handle.kill().catch(() => { }); };
@@ -333,7 +346,8 @@ export class RemoteContainerExecutionEnv {
333
346
  const fg = this.waitWithLiveness(handle, () => lastProgress, {
334
347
  waitP,
335
348
  disarmed: () => detached,
336
- ...(tee ? { wallMs: this.commandTimeoutMs(options?.timeout) } : {}),
349
+ ...(tee ? { wall: { totalMs: this.commandTimeoutMs(options?.timeout), leftMs: Math.max(0, this.commandTimeoutMs(options?.timeout) - (Date.now() - startedAt)) } } : {}),
350
+ liveness: bounds.liveness,
337
351
  });
338
352
  const winner = await (detachedP ? Promise.race([fg, detachedP]) : fg);
339
353
  if (winner === "detached") {
@@ -385,6 +399,9 @@ export class RemoteContainerExecutionEnv {
385
399
  };
386
400
  let bytes = 0;
387
401
  const cap = options?.maxOutputBytes;
402
+ const bounds = commandBoundsOf({ timeoutMs: options?.timeout != null ? options.timeout * 1000 : undefined, readTimeoutMs: options?.readTimeoutMs }, this.laneLiveness);
403
+ const liveness = bounds.liveness;
404
+ const first = firstBoundOf(bounds);
388
405
  const push = (type, data) => {
389
406
  if (finished)
390
407
  return;
@@ -394,18 +411,22 @@ export class RemoteContainerExecutionEnv {
394
411
  };
395
412
  this.activeCommands++;
396
413
  let handle;
414
+ let starting;
397
415
  try {
398
- handle = await this.rpc("commands.run", () => sbx.commands.run(command, {
416
+ handle = await this.rpc("commands.run", () => (starting = sbx.commands.run(command, {
399
417
  background: true,
400
418
  cwd: this.resolve(options?.cwd ?? this.cwd),
401
419
  ...(options?.env ? { envs: options.env } : {}),
402
420
  timeoutMs: this.commandTimeoutMs(options?.timeout),
403
421
  onStdout: (d) => push("stdout", d),
404
422
  onStderr: (d) => push("stderr", d),
405
- }), options?.signal, this.cfg.livenessMs);
423
+ })), options?.signal, first.ms);
406
424
  }
407
425
  catch (e) {
408
- this.activeCommands--;
426
+ finished = true;
427
+ this.settleAbandonedStart(starting);
428
+ if (e instanceof RemoteExecutionError && e.code === "timeout" && !options?.signal?.aborted)
429
+ throw new RemoteExecutionError("timeout", boundCutMessage(first), e);
409
430
  throw this.remoteError(e, "connect_failed");
410
431
  }
411
432
  handle
@@ -424,7 +445,7 @@ export class RemoteContainerExecutionEnv {
424
445
  exitCode = e.exitCode;
425
446
  }
426
447
  else {
427
- failure = this.remoteError(e, "unknown");
448
+ failure = this.remoteError(e, providerTimedOut(e) ? "timeout" : "unknown");
428
449
  }
429
450
  finished = true;
430
451
  signalReady();
@@ -465,13 +486,14 @@ export class RemoteContainerExecutionEnv {
465
486
  const next = new Promise((resolve) => {
466
487
  wake = resolve;
467
488
  });
468
- const idleMs = options?.readTimeoutMs ?? this.cfg.livenessMs;
469
489
  let timer;
470
- const idle = new Promise((_, reject) => {
471
- timer = setTimeout(() => reject(new RemoteExecutionError("timeout", `execStream idle > ${idleMs}ms (provider hang / unreachable sandbox)`)), idleMs);
472
- });
490
+ const idle = liveness === undefined
491
+ ? undefined
492
+ : new Promise((_, reject) => {
493
+ timer = setTimeout(() => reject(new RemoteExecutionError("timeout", livenessCutMessage(liveness))), liveness.ms);
494
+ });
473
495
  try {
474
- await Promise.race([next, idle]);
496
+ await (idle === undefined ? next : Promise.race([next, idle]));
475
497
  }
476
498
  catch (e) {
477
499
  finished = true;
@@ -794,36 +816,59 @@ export class RemoteContainerExecutionEnv {
794
816
  combined?.dispose();
795
817
  }
796
818
  }
819
+ settleAbandonedStart(starting) {
820
+ let held = true;
821
+ const release = () => {
822
+ if (!held)
823
+ return;
824
+ held = false;
825
+ this.activeCommands--;
826
+ };
827
+ if (starting === undefined)
828
+ return release();
829
+ void starting.then((late) => {
830
+ late.wait().then(release, (e) => {
831
+ if (e instanceof CommandExitError)
832
+ release();
833
+ });
834
+ late.kill().then(release, (e) => {
835
+ this.cfg.logger?.warn?.("e2b_abandoned_start_kill_failed", { sandboxId: this.sbx?.sandboxId, pid: late.pid, error: e instanceof Error ? e.name : typeof e });
836
+ });
837
+ }, release);
838
+ }
797
839
  commandTimeoutMs(timeoutSec) {
798
840
  return timeoutSec != null ? timeoutSec * 1000 : this.cfg.timeoutMs;
799
841
  }
800
842
  async waitWithLiveness(handle, lastProgress, opts) {
801
843
  let timer;
802
844
  let wallTimer;
845
+ const liveness = opts?.liveness;
803
846
  const idle = new Promise((_, reject) => {
804
- const arm = () => {
847
+ const arm = (bound) => {
805
848
  timer = setTimeout(() => {
806
849
  if (opts?.disarmed?.())
807
850
  return;
808
851
  const idleFor = Date.now() - lastProgress();
809
- if (idleFor >= this.cfg.livenessMs) {
852
+ if (idleFor >= bound.ms) {
853
+ reject(new ExecutionError("timeout", livenessCutMessage(bound)));
810
854
  void handle.kill().catch(() => { });
811
855
  this.cfg.metrics?.inc("remote_env_exec_idle_timeout_total", { provider: PROVIDER });
812
- reject(new RemoteExecutionError("timeout", `exec idle > ${this.cfg.livenessMs}ms (provider hang / unreachable sandbox)`));
813
856
  }
814
857
  else {
815
- arm();
858
+ arm(bound);
816
859
  }
817
- }, Math.max(this.cfg.livenessMs - (Date.now() - lastProgress()), 0));
860
+ }, Math.max(bound.ms - (Date.now() - lastProgress()), 0));
818
861
  };
819
- arm();
820
- if (opts?.wallMs != null) {
862
+ if (liveness !== undefined)
863
+ arm(liveness);
864
+ if (opts?.wall !== undefined) {
865
+ const wall = opts.wall;
821
866
  wallTimer = setTimeout(() => {
822
867
  if (opts.disarmed?.())
823
868
  return;
869
+ reject(new ExecutionError("timeout", boundCutMessage({ ms: wall.totalMs, cut: "wall" })));
824
870
  void handle.kill().catch(() => { });
825
- reject(new RemoteExecutionError("timeout", `exec exceeded ${opts.wallMs}ms`));
826
- }, opts.wallMs);
871
+ }, wall.leftMs);
827
872
  }
828
873
  });
829
874
  try {
@@ -905,7 +950,7 @@ export class RemoteContainerExecutionEnv {
905
950
  const name = e?.name;
906
951
  if (abortSignal?.aborted || name === "AbortError")
907
952
  return new ExecutionError("aborted", msg);
908
- if (name === "TimeoutError" || /timeout|timed out/i.test(msg))
953
+ if (providerTimedOut(e))
909
954
  return new ExecutionError("timeout", msg);
910
955
  return new ExecutionError("unknown", msg, e instanceof Error ? e : undefined);
911
956
  }
@@ -924,6 +969,12 @@ export function e2bExecutionEnvFactory(config, deps) {
924
969
  function errMsg(e) {
925
970
  return e instanceof Error ? e.message : String(e);
926
971
  }
972
+ function boundCutMessage(first) {
973
+ return first.cut === "wall" ? `exec exceeded ${first.ms}ms` : livenessCutMessage(first.cut);
974
+ }
975
+ function providerTimedOut(e) {
976
+ return e?.name === "TimeoutError" || /timeout|timed out/i.test(errMsg(e));
977
+ }
927
978
  function anySignal(signals) {
928
979
  const ctor = AbortSignal;
929
980
  if (typeof ctor.any === "function")
@@ -1,4 +1,4 @@
1
- import { FileError, ExecutionError, RemoteExecutionError, SchedulerError, type SchedulerCapability, type ScheduledIntent, type SchedulerContext, type ScheduledTaskId, type ScheduledTaskSummary, type ExecutionEnv, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type SymlinkChain, type BoundFileObject, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type VmLifecycleOptions, type ExecutionEnvFactory, BackgroundShellError, type BackgroundShellCapability, type BackgroundShellId, type BackgroundHardKillOutcome, type BackgroundPoll, type BackgroundSpawnOptions } from "@sema-agent/core";
1
+ import { FileError, type InheritEnvPolicy, ExecutionError, RemoteExecutionError, SchedulerError, type SchedulerCapability, type ScheduledIntent, type SchedulerContext, type ScheduledTaskId, type ScheduledTaskSummary, type ExecutionEnv, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type SymlinkChain, type BoundFileObject, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type VmLifecycleOptions, type ExecutionEnvFactory, BackgroundShellError, type BackgroundShellCapability, type BackgroundShellId, type BackgroundHardKillOutcome, type BackgroundPoll, type BackgroundSpawnOptions } from "@sema-agent/core";
2
2
  /** Options accepted by `ExecutionEnv.exec` (core does not re-export the type; derive it from the seam). */
3
3
  type ExecOpts = Parameters<ExecutionEnv["exec"]>[1];
4
4
  /** SIGTERM → grace → SIGKILL (no host isolation). 🔴 宽限的**唯一属主**:本 driver 的 `kill` 梯子用它排升级定时器;会话级收敛
@@ -35,18 +35,17 @@ export interface HostEnvConfig {
35
35
  homeDir?: string;
36
36
  /** Per-command wall-clock bound (ms) when the caller gives none. Default {@link DEFAULT_COMMAND_TIMEOUT_MS}. */
37
37
  commandTimeoutMs?: number;
38
- /** Shell-child env policy (D1; mirrors core's `NodeExecutionEnv`). Default `"scrub"`: inherit the worker's
39
- * `process.env` but DROP secret-pattern keys (core's `scrubSecretEnv` — `*_KEY`/`*_TOKEN`/`*_SECRET`/`*_PASSWORD`…)
40
- * AND every env name the server's config catalog types as `secret` (`server-secret-env.ts`; 7.99.0 F7 —
41
- * `SERVICE_AUTH_TOKENS` / `APPROVAL_HMAC_KEYS` slip the shape rule)
42
- * so a model-driven shell can't `printenv DEEPSEEK_API_KEY` and exfiltrate orchestrator secrets — keeps PATH/HOME/
43
- * locale/build (low breakage). `"all"` = inherit everything (explicit opt-out, trusted local dev). A `string[]` =
44
- * strict (hermetic) allowlist via `allowlistedEnv` — the repo's one "named keys pass verbatim" predicate, shared with
45
- * the local language server's `LSP_ENV_ALLOW` (S-699; that leg puts named keys back ON TOP of the scrub instead).
46
- * `env`/per-command `options.env` ALWAYS pass through verbatim. */
47
- inheritEnv?: "scrub" | "all" | string[];
48
- /** @deprecated use {@link inheritEnv}. `true`→`"all"`, `false`→`[]` (hermetic). Ignored if `inheritEnv` is set. */
49
- inheritProcessEnv?: boolean;
38
+ /** Shell-child env policy — core's `InheritEnvPolicy`, read by core's `inheritedHostEnv` (the three words are judged
39
+ * in ONE place, the engine; this adapter never reads them). Default `"scrub"`: inherit the worker's `process.env`
40
+ * minus credential-shaped names (core's shape rules) minus this deployment's OWN credentials (every env name the
41
+ * server's config catalog types as `secret` + the name-list knobs + the effective model entries' `apiKeyEnv` — declared at boot and
42
+ * re-declared before every config generation lands, names only ever added, `declareServerWithheldEnv`),
43
+ * so a model-driven shell can't `printenv DEEPSEEK_API_KEY` — keeps PATH/HOME/locale/build (low breakage).
44
+ * `"all"` = the whole environment MINUS the declared own credentials (the user's own `GH_TOKEN` flows, the engine's
45
+ * `SERVICE_AUTH_TOKEN` never does — 7.129.0 / core 7.51.1). A `string[]` = strict (hermetic) allowlist; naming a declared
46
+ * own credential is REFUSED at construction (`inheritEnvPolicyProblem`) and again at every spawn. `env`/per-command
47
+ * `options.env` are the adapter's explicit grant and ALWAYS pass through verbatim. */
48
+ inheritEnv?: InheritEnvPolicy;
50
49
  /**
51
50
  * EXACT persistent working directory (TOC, CC-like): when set, the adapter uses THIS dir verbatim — NO
52
51
  * `sema-host-<id>-<rand>` per-task subdir — and **NEVER `rm -rf`s it on {@link destroy}**. It is the
@@ -419,21 +418,19 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
419
418
  * 同步 `existsSync` 而不是 await:这是拆除路径上的判据,不该再引入一次异步让出。 */
420
419
  private isLingeringMirrorTarget;
421
420
  private fsReady;
422
- /** Base env (process.env when inheriting) merged with the adapter's secret env, then per-command overrides win. */
423
- /** D1 secure default = `"scrub"`: inherit `process.env` minus secret-pattern keys (core's `scrubSecretEnv`, single
424
- * source of the shape rule) minus the config catalog's `secret`-typed names (`scrubServerSecrets`, 7.99.0 F7). `"all"` = full inherit (opt-out); a `string[]` = strict allowlist. Legacy `inheritProcessEnv`
425
- * maps `true`→`"all"` / `false`→`[]`. */
426
- private inheritMode;
427
421
  /** 每次 spawn 交给子进程的 env(组合 + 剥除披露 + win32 的 SHELL 钉)。`sh` = 调用点刚从 {@link hostShell} 读到的那只(spawn 路径
428
422
  * 都在 `shellReady()` 之后):没有已落定的 shell 就到不了这条腿 —— 类型即次序守卫。 */
429
423
  private mergeEnv;
430
424
  /**
431
425
  * 子进程 env 的**唯一组合**,分两层(一条律:**env 组合是纯函数,shell 只属于 spawn**):
432
- * · **组合层**(恒有)—— 继承姿态 + `cfg.env` + perCommand + win32 大小写折叠。**不碰 shell**,所以读面(如 {@link cdPath})任何时候
426
+ * · **组合层**(恒有)—— 继承面 + `cfg.env` + perCommand + win32 大小写折叠。继承面 = 本仓唯一组合(`server-secret-env.ts`)交引擎
427
+ * `inheritedHostEnv(policy)`:三个继承词(`"all"` / `"scrub"` / `[names]`)只在引擎一处判,本进程自有凭据名(起服声明)任何词都不交 ——
428
+ * 本类不再有三臂字面(7.129.0 / core 7.51.1;修前 `all` 是 `{ ...process.env }` 零剥除)。**不碰 shell**,所以读面(如 {@link cdPath})任何时候
433
429
  * 都能读,不依赖 shell 是否已落定。
434
- * · **spawn 层**(`spawnShell` 在场)—— 这一次是真要交给子进程:scrub 腿经 `hostSubprocessEnv` 记剥除(`spawnShell` 缺席 ⇒ 只读组合,
430
+ * · **spawn 层**(`spawnShell` 在场)—— 这一次是真要交给子进程:经 `hostSubprocessEnv` 记剥除(`spawnShell` 缺席 ⇒ 只读组合 `composeHostSubprocessEnv`,
435
431
  * 不计数:一次没发生的 spawn 不进剥除账),win32 上把 `SHELL` 钉到已解析的 Git Bash({@link pinHostShell},在大小写折叠**之前**)。
436
432
  * 披露与钉同一个开关(`spawnShell`)—— 「真 spawn」只有一种说法,不再有 `disclose` 与钉各自一个布尔。
433
+ * 引擎对点到自有凭据名的白名单在这里**抛**(spawn 时的拒;构造器已先问过同一只判)—— 子进程不起,拒句带名。
437
434
  */
438
435
  private childEnv;
439
436
  private fileError;
@@ -6,11 +6,12 @@ import { randomBytes } from "node:crypto";
6
6
  import { openSync, closeSync, readSync, statSync, truncateSync, unlinkSync, mkdirSync, rmdirSync, mkdtempSync, existsSync, realpathSync, accessSync, constants as FS_CONST } from "node:fs";
7
7
  import { StringDecoder } from "node:string_decoder";
8
8
  import { armPipeDestroyGrace, numEnvOr, SYMLINK_CHAIN_MAX_HOPS } from "./remote-shell.js";
9
+ import { commandBoundsOf, livenessCutMessage } from "../exec-bounds.js";
9
10
  import { hostBackgroundShellEnabled, hostExecSpoolEnabled } from "../config.js";
10
11
  import { IS_WIN32, resolveHostShell, hostShell, pinHostShell, spawnGroupOptions, killTreeHard, killTreeSoft, hardKillGroupNow, collapseWin32EnvKeys, adoptHostForegroundChild } from "./host-platform.js";
11
12
  import { BackgroundShellManager, seedMemStream, feedMemStream, drainMemStream } from "./background-shell-support.js";
12
- import { FileError, NodeExecutionEnv, ExecutionError, RemoteExecutionError, RollingTailBuffer, markTruncated, SchedulerError, BackgroundShellError, } from "@sema-agent/core";
13
- import { allowlistedEnv, composeHostSubprocessEnv, hostSubprocessEnv } from "../server-secret-env.js";
13
+ import { FileError, NodeExecutionEnv, inheritEnvPolicyProblem, ExecutionError, RemoteExecutionError, RollingTailBuffer, markTruncated, SchedulerError, BackgroundShellError, } from "@sema-agent/core";
14
+ import { composeHostSubprocessEnv, hostSubprocessEnv } from "../server-secret-env.js";
14
15
  import { trackEphemeralHostWorkspace, untrackEphemeralHostWorkspace } from "./host-workspace-registry.js";
15
16
  const PROVIDER = "host";
16
17
  const DEFAULT_COMMAND_TIMEOUT_MS = 30 * 60_000;
@@ -122,10 +123,18 @@ export class RemoteHostExecutionEnv {
122
123
  this.workspaceDir = path.resolve(base, `sema-host-${sanitizeId(id)}-${suffix}`);
123
124
  this.persistent = false;
124
125
  }
126
+ const inheritEnvProblem = inheritEnvPolicyProblem(config.inheritEnv);
127
+ if (inheritEnvProblem !== undefined)
128
+ throw new Error(`RemoteHostExecutionEnv: ${inheritEnvProblem}`);
129
+ if ("inheritProcessEnv" in config) {
130
+ throw new Error('RemoteHostExecutionEnv: inheritProcessEnv was retired in 7.129.0 and is refused rather than read — write inheritEnv: "all" (for true) or inheritEnv: [] (for false)');
131
+ }
125
132
  this.cfg = {
126
133
  ...config,
127
134
  commandTimeoutMs: config.commandTimeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS,
128
135
  };
136
+ if (Array.isArray(config.inheritEnv))
137
+ this.cfg.inheritEnv = [...config.inheritEnv];
129
138
  this.cwd = this.workspaceDir;
130
139
  this.homeDir = config.homeDir;
131
140
  this.handle = { sandboxId: this.workspaceDir, provider: PROVIDER, mountPath: this.workspaceDir };
@@ -802,8 +811,9 @@ export class RemoteHostExecutionEnv {
802
811
  if (!ready.ok)
803
812
  throw new RemoteExecutionError("connect_failed", ready.error.message, ready.error);
804
813
  const cwd = this.resolve(options?.cwd ?? this.cwd);
805
- const timeoutMs = options?.timeout != null ? options.timeout * 1000 : undefined;
806
- const idleMs = options?.readTimeoutMs;
814
+ const bounds = commandBoundsOf({ timeoutMs: options?.timeout != null ? options.timeout * 1000 : undefined, readTimeoutMs: options?.readTimeoutMs }, undefined);
815
+ const timeoutMs = bounds.wallMs;
816
+ const liveness = bounds.liveness;
807
817
  const cap = options?.maxOutputBytes;
808
818
  const queue = [];
809
819
  let finished = false;
@@ -897,7 +907,7 @@ export class RemoteHostExecutionEnv {
897
907
  throw failure;
898
908
  if (finished)
899
909
  break;
900
- const waitMs = idleMs != null ? Math.max(1, idleMs - (Date.now() - lastChunk)) : 30_000;
910
+ const waitMs = liveness !== undefined ? Math.max(1, liveness.ms - (Date.now() - lastChunk)) : 30_000;
901
911
  let waitTimer;
902
912
  try {
903
913
  await new Promise((r) => { wake = r; waitTimer = setTimeout(r, waitMs); });
@@ -906,10 +916,10 @@ export class RemoteHostExecutionEnv {
906
916
  if (waitTimer !== undefined)
907
917
  clearTimeout(waitTimer);
908
918
  }
909
- if (idleMs != null && !finished && Date.now() - lastChunk >= idleMs) {
919
+ if (liveness !== undefined && !finished && Date.now() - lastChunk >= liveness.ms) {
910
920
  finished = true;
911
921
  kill();
912
- throw new RemoteExecutionError("timeout", `execStream idle > ${idleMs}ms (command hang)`);
922
+ throw new RemoteExecutionError("timeout", livenessCutMessage(liveness));
913
923
  }
914
924
  }
915
925
  while (queue.length)
@@ -1310,33 +1320,13 @@ export class RemoteHostExecutionEnv {
1310
1320
  return { ok: false, error: new FileError("unknown", `failed to create host workspace: ${errMsg(e)}`, this.resolve(p), asError(e)) };
1311
1321
  }
1312
1322
  }
1313
- inheritMode() {
1314
- if (this.cfg.inheritEnv !== undefined)
1315
- return this.cfg.inheritEnv;
1316
- if (this.cfg.inheritProcessEnv === false)
1317
- return [];
1318
- if (this.cfg.inheritProcessEnv === true)
1319
- return "all";
1320
- return "scrub";
1321
- }
1322
1323
  mergeEnv(perCommand, sh) {
1323
1324
  return this.childEnv(perCommand, sh);
1324
1325
  }
1325
1326
  childEnv(perCommand, spawnShell) {
1326
- const disclose = spawnShell !== undefined;
1327
- const mode = this.inheritMode();
1328
- let base;
1329
- if (mode === "all")
1330
- base = { ...process.env };
1331
- else if (Array.isArray(mode)) {
1332
- base = allowlistedEnv(mode, process.env);
1333
- }
1334
- else if (!disclose) {
1335
- base = composeHostSubprocessEnv([]);
1336
- }
1337
- else {
1338
- base = hostSubprocessEnv({ site: "server.remote-env-host.inherit-scrub", allow: [] });
1339
- }
1327
+ const base = spawnShell !== undefined
1328
+ ? hostSubprocessEnv({ site: "server.remote-env-host.inherit-scrub", policy: this.cfg.inheritEnv, allow: [] })
1329
+ : composeHostSubprocessEnv({ policy: this.cfg.inheritEnv, allow: [] });
1340
1330
  if (this.cfg.env)
1341
1331
  Object.assign(base, this.cfg.env);
1342
1332
  if (perCommand)
@@ -38,7 +38,9 @@ export interface K8sEnvConfig {
38
38
  readyTimeoutMs?: number;
39
39
  /** Control-plane RPC wall-clock (pod create/get/delete). */
40
40
  rpcTimeoutMs?: number;
41
- /** Zero-progress liveness bound for commands (no output AND not finished ⇒ hang). */
41
+ /** The lane's zero-progress liveness bound (env `K8S_LIVENESS_MS`; default {@link LIVENESS_MS_DEFAULT}): a command that
42
+ * produces no output for this long and has not finished is cut as a hang — ONLY when its caller stated neither a wall-clock
43
+ * (`timeout`) nor its own idle bound (`readTimeoutMs`); a stated bound replaces it (the rule: `exec-bounds.ts`). */
42
44
  livenessMs?: number;
43
45
  /** The ONE data bound (`WORKSPACE_SNAPSHOT_TIMEOUT_MS`; default = `WORKSPACE_SNAPSHOT_TIMEOUT_MS_DEFAULT`, owned by
44
46
  * `workspace-space.ts`): the snapshot upload's and restore's wall-clock, the deadline this replica's commits write into
@@ -257,6 +259,8 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
257
259
  /** The known occupant's incarnation (`undefined` = this run has no known occupant: a delegated child, an unread session, a direct construction) — the
258
260
  * ownership mark of every snapshot id this env mints, both lanes ({@link mintSnapshotId}); the space lane's partition is the same reading. */
259
261
  private readonly occupantIncarnation;
262
+ /** This lane's own liveness bound — what {@link commandBoundsOf} falls back to when a command's caller stated no bound. */
263
+ private readonly laneLiveness;
260
264
  /** This env's space lane (resolved once from the policy + the occupant's incarnation; see {@link SpaceLane}). */
261
265
  private readonly spaceLane;
262
266
  /** `workspace_space_unavailable` is said once per env. */
@@ -551,8 +555,10 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
551
555
  private execUrl;
552
556
  /**
553
557
  * Run a command over the exec WebSocket. Frames: byte0=channel (1 stdout / 2 stderr / 3 status JSON with the
554
- * REAL exit code). Bounds: caller wall-clock (explicit `timeout`) + zero-progress liveness (`livenessMs`) —
555
- * a socket killed by either does NOT kill the remote process (disposable-pod posture, see header).
558
+ * REAL exit code). Bounds = {@link commandBoundsOf} over what the caller stated (`timeoutMs` wall / `readTimeoutMs` idle)
559
+ * and this lane's liveness — the one rule, decided here and nowhere else on this lane — plus the adapter's own ceiling
560
+ * `capMs` (a data bound, not a statement: it never turns the liveness bound off). A socket killed by any bound does NOT
561
+ * kill the remote process (disposable-pod posture, see header).
556
562
  */
557
563
  private runExec;
558
564
  exec(command: string, options?: ExecOpts): Promise<Result<{
@@ -654,7 +660,9 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
654
660
  suffix?: string;
655
661
  abortSignal?: AbortSignal;
656
662
  }): Promise<Result<string, FileError>>;
657
- /** exec with the data-transfer bound (large reads/writes legitimately run longer than a control op). */
663
+ /** exec with the data-transfer ceiling (large reads/writes legitimately run longer than a control op). The engine states no
664
+ * bound for a file op, so the lane liveness bound stays on (a stalled transfer is cut by it); the data bound is this
665
+ * adapter's own ceiling (`capMs`), not a statement. */
658
666
  private execData;
659
667
  /** 本腿只定工作目录(缺省 cwd 规则 + 绝对化);串的形状归 {@link buildCwdEnvCommand} / {@link createCwdGuardedCall}(S-410:cwd 不可用 ⇒ 命令不跑;
660
668
  * 车PL:buffered `exec` 上守卫触发 ⇒ `spawn_error`)。 */
@@ -11,6 +11,7 @@ import { spaceCommitAlive } from "../workspace-space-commit.js";
11
11
  import { incarnationLogPrefix, redactIncarnations } from "../session-incarnation.js";
12
12
  import { WORKSPACE_CACHE_SPACE_LABEL, workspaceCacheClaimOf, workspaceCacheEnv } from "../workspace-cache.js";
13
13
  import { recordFailOpen } from "../observability/fail-open.js";
14
+ import { commandBoundsOf, laneLivenessOf, livenessCutMessage, LIVENESS_MS_DEFAULT } from "../exec-bounds.js";
14
15
  import { SANDBOX_IPV6_DEFAULT, sandboxEgressGaps, sandboxPodSysctls } from "../sandbox-egress-policy.js";
15
16
  import { FileError, ExecutionError, RemoteExecutionError, withRetry, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
16
17
  import { fileErrorFromExec, classifyFsStderr } from "./remote-env-file-error.js";
@@ -141,6 +142,7 @@ export class RemoteK8sExecutionEnv {
141
142
  cfg;
142
143
  deps;
143
144
  occupantIncarnation;
145
+ laneLiveness;
144
146
  spaceLane;
145
147
  spaceUnavailableSaid = false;
146
148
  unreadableOccupantSaid = false;
@@ -161,7 +163,7 @@ export class RemoteK8sExecutionEnv {
161
163
  timeoutMs: config.timeoutMs ?? 30 * 60_000,
162
164
  readyTimeoutMs: config.readyTimeoutMs ?? 120_000,
163
165
  rpcTimeoutMs: config.rpcTimeoutMs ?? 30_000,
164
- livenessMs: config.livenessMs ?? 120_000,
166
+ livenessMs: config.livenessMs ?? LIVENESS_MS_DEFAULT,
165
167
  dataTimeoutMs: dataTimeoutMsOf(config),
166
168
  cpu: config.cpu ?? "1",
167
169
  memory: config.memory ?? "2Gi",
@@ -176,6 +178,7 @@ export class RemoteK8sExecutionEnv {
176
178
  this.cwd = this.cfg.mountPath;
177
179
  this.homeDir = config.homeDir;
178
180
  this.deps = deps ?? {};
181
+ this.laneLiveness = laneLivenessOf(this.cfg.livenessMs, "K8S_LIVENESS_MS");
179
182
  const occupant = occupantKeyOf(config.spaceOccupant);
180
183
  this.occupantIncarnation = "incarnation" in occupant ? occupant.incarnation : undefined;
181
184
  this.spaceLane = spaceLaneOf(config.s3Snapshot?.space, config.spaceOccupant);
@@ -875,7 +878,7 @@ export class RemoteK8sExecutionEnv {
875
878
  const excludes = (this.cfg.snapshotExclude ?? []).map((item) => ` --exclude=${shellQuote(`./${item}`)}`).join("");
876
879
  const cmd = `tar czf ${tmp}${excludes} -C ${shellQuote(this.cfg.mountPath)} . && curl -fsS -X PUT -T ${tmp} ${shellQuote(url)}; ` +
877
880
  `rc=$?; rm -f ${tmp}; exit $rc`;
878
- const r = await this.runExec(cmd, { timeoutMs: this.cfg.dataTimeoutMs, livenessMs: 0 });
881
+ const r = await this.runExec(cmd, { timeoutMs: this.cfg.dataTimeoutMs });
879
882
  if (!r.ok)
880
883
  return { ok: false, error: new RemoteExecutionError("connect_failed", `workspace snapshot upload failed: ${this.redactUrl(r.error.message, url)}`, r.error) };
881
884
  if (r.value.exitCode !== 0)
@@ -911,7 +914,7 @@ export class RemoteK8sExecutionEnv {
911
914
  const c = await this.provision(abortSignal);
912
915
  if (!c.ok)
913
916
  return c;
914
- const r = await this.runExec(cmd, { timeoutMs: this.cfg.dataTimeoutMs, livenessMs: 0 });
917
+ const r = await this.runExec(cmd, { timeoutMs: this.cfg.dataTimeoutMs });
915
918
  const failure = !r.ok
916
919
  ? { cls: r.error.code === "timeout" ? "timeout" : "transport", detail: `workspace snapshot restore failed: ${this.redactUrl(r.error.message, url)}`, cause: r.error }
917
920
  : r.value.exitCode !== 0
@@ -1127,20 +1130,22 @@ export class RemoteK8sExecutionEnv {
1127
1130
  }
1128
1131
  resolvePromise(r);
1129
1132
  };
1130
- const wallMs = opts?.timeoutMs;
1131
- const wallTimer = wallMs ? setTimeout(() => finish({ ok: false, error: new ExecutionError("timeout", `command exceeded ${wallMs}ms`) }), wallMs) : undefined;
1133
+ const bounds = commandBoundsOf({ timeoutMs: opts?.timeoutMs, readTimeoutMs: opts?.readTimeoutMs }, this.laneLiveness);
1134
+ const capMs = opts?.capMs;
1135
+ const wallMs = bounds.wallMs === undefined ? capMs : capMs === undefined ? bounds.wallMs : Math.min(bounds.wallMs, capMs);
1136
+ const wallTimer = wallMs !== undefined ? setTimeout(() => finish({ ok: false, error: new ExecutionError("timeout", `command exceeded ${wallMs}ms`) }), wallMs) : undefined;
1132
1137
  let livenessTimer;
1133
- const livenessMs = opts?.livenessMs ?? this.cfg.livenessMs;
1138
+ const liveness = bounds.liveness;
1134
1139
  const armLiveness = () => {
1135
- if (livenessMs <= 0)
1140
+ if (liveness === undefined)
1136
1141
  return;
1137
1142
  clearTimeout(livenessTimer);
1138
1143
  const idle = Date.now() - lastProgress;
1139
- if (idle >= livenessMs) {
1140
- finish({ ok: false, error: new ExecutionError("timeout", `no output for ${idle}ms (exec liveness) — suspected hang`) });
1144
+ if (idle >= liveness.ms) {
1145
+ finish({ ok: false, error: new ExecutionError("timeout", livenessCutMessage(liveness)) });
1141
1146
  return;
1142
1147
  }
1143
- livenessTimer = setTimeout(armLiveness, livenessMs - idle);
1148
+ livenessTimer = setTimeout(armLiveness, liveness.ms - idle);
1144
1149
  };
1145
1150
  armLiveness();
1146
1151
  const onAbort = () => finish({ ok: false, error: new ExecutionError("aborted", "command aborted") });
@@ -1343,22 +1348,20 @@ export class RemoteK8sExecutionEnv {
1343
1348
  wake = undefined;
1344
1349
  w?.();
1345
1350
  };
1346
- let lastChunk = Date.now();
1347
- const idleMs = options?.readTimeoutMs ?? this.cfg.livenessMs;
1348
1351
  const cap = options?.maxOutputBytes;
1349
1352
  let bytes = 0;
1350
1353
  const ac = new AbortController();
1351
1354
  const signal = options?.signal ? AbortSignal.any([options.signal, ac.signal]) : ac.signal;
1352
1355
  const done = this.runExec(buildCwdEnvCommand(command, this.workingDirOf(options?.cwd), options?.env, "raw").command, {
1353
1356
  signal,
1357
+ timeoutMs: options?.timeout != null ? options.timeout * 1000 : undefined,
1358
+ readTimeoutMs: options?.readTimeoutMs,
1354
1359
  boundOutput: true,
1355
1360
  onStdout: (d) => {
1356
- lastChunk = Date.now();
1357
1361
  queue.push({ type: "stdout", data: d });
1358
1362
  signalReady();
1359
1363
  },
1360
1364
  onStderr: (d) => {
1361
- lastChunk = Date.now();
1362
1365
  queue.push({ type: "stderr", data: d });
1363
1366
  signalReady();
1364
1367
  },
@@ -1395,21 +1398,9 @@ export class RemoteK8sExecutionEnv {
1395
1398
  throw failure;
1396
1399
  if (finished)
1397
1400
  break;
1398
- const waitMs = Math.max(1, idleMs - (Date.now() - lastChunk));
1399
- let waitTimer;
1400
- try {
1401
- await new Promise((r) => {
1402
- wake = r;
1403
- waitTimer = setTimeout(r, Math.min(waitMs, 30_000));
1404
- });
1405
- }
1406
- finally {
1407
- if (waitTimer !== undefined)
1408
- clearTimeout(waitTimer);
1409
- }
1410
- if (!finished && Date.now() - lastChunk >= idleMs) {
1411
- throw new RemoteExecutionError("timeout", `execStream idle > ${idleMs}ms (suspected hang)`);
1412
- }
1401
+ await new Promise((r) => {
1402
+ wake = r;
1403
+ });
1413
1404
  }
1414
1405
  while (queue.length)
1415
1406
  yield queue.shift();
@@ -1428,7 +1419,7 @@ export class RemoteK8sExecutionEnv {
1428
1419
  }
1429
1420
  makeBgDriver() {
1430
1421
  const self = this;
1431
- const bgExec = (script, boundOutput) => self.runExec(script, { timeoutMs: self.cfg.rpcTimeoutMs, livenessMs: self.cfg.livenessMs, boundOutput });
1422
+ const bgExec = (script, boundOutput) => self.runExec(script, { timeoutMs: self.cfg.rpcTimeoutMs, boundOutput });
1432
1423
  return {
1433
1424
  async launch(command, options, ctx) {
1434
1425
  const conn = await self.ensureConnected();
@@ -1628,7 +1619,7 @@ export class RemoteK8sExecutionEnv {
1628
1619
  const c = await this.ensureConnected();
1629
1620
  if (!c.ok)
1630
1621
  return c;
1631
- return this.runExec(command, { signal: abortSignal, timeoutMs: this.cfg.dataTimeoutMs });
1622
+ return this.runExec(command, { signal: abortSignal, capMs: this.cfg.dataTimeoutMs });
1632
1623
  }
1633
1624
  workingDirOf(cwd) {
1634
1625
  const dir = cwd ?? this.cwd;