@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.
- package/MIGRATION.md +140 -0
- package/USAGE.md +114 -18
- package/dist/approval-card.d.ts +53 -26
- package/dist/approval-card.js +40 -11
- package/dist/approval-crash-convergence.js +4 -4
- package/dist/bake-runner/main.js +2 -1
- package/dist/boot/execution-env.js +1 -0
- package/dist/boot/memory-consolidation.d.ts +46 -16
- package/dist/boot/memory-consolidation.js +15 -8
- package/dist/boot/side-query-lane.d.ts +7 -20
- package/dist/boot/side-query-lane.js +0 -24
- package/dist/boot/stage-01-config.js +3 -0
- package/dist/boot/stage-06-runners.js +1 -1
- package/dist/boot/stores.js +2 -2
- package/dist/boot/web-admin.js +17 -10
- package/dist/boot/webfetch-summarize-lane.d.ts +10 -7
- package/dist/boot/webfetch-summarize-lane.js +2 -3
- package/dist/brain.js +6 -9
- package/dist/capabilities/builtin-tools.d.ts +2 -2
- package/dist/capabilities/engine-capabilities.d.ts +14 -0
- package/dist/capabilities/engine-capabilities.js +12 -0
- package/dist/capability-facts.d.ts +3 -1
- package/dist/capability-facts.js +1 -0
- package/dist/config-catalog.js +5 -1
- package/dist/config-types.d.ts +7 -3
- package/dist/config.js +7 -1
- package/dist/egress-redirect.d.ts +12 -0
- package/dist/egress-redirect.js +7 -0
- package/dist/entry-mode-clamp-notice.d.ts +30 -0
- package/dist/entry-mode-clamp-notice.js +13 -0
- package/dist/exec-bounds.d.ts +69 -0
- package/dist/exec-bounds.js +37 -0
- package/dist/fleet/fleet-bus.d.ts +7 -0
- package/dist/fleet/fleet-bus.js +1 -0
- package/dist/hooks/hook-llm.js +6 -2
- package/dist/host-lsp-manager.d.ts +3 -3
- package/dist/http/admission.js +5 -2
- package/dist/http/resume-legs.js +46 -27
- package/dist/http/route-ctx.d.ts +1 -1
- package/dist/http/routes/approvals-assistant.js +6 -4
- package/dist/http/routes/capabilities.js +1 -0
- package/dist/http/routes/session-sync.js +3 -4
- package/dist/http/routes/sessions.js +4 -17
- package/dist/http/send.d.ts +1 -1
- package/dist/http/send.js +4 -3
- package/dist/http/wire-types.d.ts +3 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/observability/fail-open.d.ts +7 -3
- package/dist/observability/fail-open.js +7 -3
- package/dist/observability/secret-env-scrub.d.ts +37 -3
- package/dist/observability/secret-env-scrub.js +2 -2
- package/dist/parked-decide.js +5 -2
- package/dist/plugins/memory-embedder.js +5 -0
- package/dist/plugins/one-time-migrations.d.ts +180 -9
- package/dist/plugins/one-time-migrations.js +315 -3
- package/dist/plugins/pg-pool.js +3 -1
- package/dist/plugins/remote-env-e2b.d.ts +31 -9
- package/dist/plugins/remote-env-e2b.js +77 -26
- package/dist/plugins/remote-env-host.d.ts +17 -20
- package/dist/plugins/remote-env-host.js +20 -30
- package/dist/plugins/remote-env-k8s.d.ts +12 -4
- package/dist/plugins/remote-env-k8s.js +22 -31
- package/dist/plugins/session-policy-store-sql.d.ts +16 -4
- package/dist/plugins/session-policy-store-sql.js +17 -20
- package/dist/plugins/session-rules-column.d.ts +16 -0
- package/dist/plugins/session-rules-column.js +6 -0
- package/dist/plugins/tidb-pool.js +4 -1
- package/dist/read-face-posture.d.ts +2 -1
- package/dist/request-value-closure.js +1 -1
- package/dist/retired-card-words.d.ts +17 -0
- package/dist/retired-card-words.js +49 -0
- package/dist/rule-entry-list.js +2 -0
- package/dist/run-local.js +10 -3
- package/dist/server-secret-env.d.ts +77 -85
- package/dist/server-secret-env.js +40 -26
- package/dist/session-policy-wire.d.ts +61 -124
- package/dist/session-policy-wire.js +59 -121
- package/dist/session-sync.d.ts +8 -6
- package/dist/session-sync.js +6 -5
- package/dist/spec-fields.d.ts +3 -2
- package/dist/standing-answer-grant.d.ts +17 -9
- package/dist/standing-answer-grant.js +11 -8
- package/dist/task-settings.d.ts +6 -1
- package/dist/task-settings.js +1 -1
- package/dist/tool-approval.d.ts +14 -7
- package/dist/tool-approval.js +23 -18
- package/dist/trace/core-keyset-guard.d.ts +3 -3
- package/dist/trace/engine-notice-wire.d.ts +1 -1
- package/dist/trace/engine-notice-wire.js +1 -0
- package/dist/trace/project.d.ts +7 -4
- package/dist/trace/project.js +6 -75
- package/dist/trace/tool-output-media.d.ts +2 -1
- package/dist/trace/tool-output-media.js +3 -25
- package/dist/web-admin-client/apply-effective.js +28 -11
- package/dist/web-admin-client/facade.d.ts +1 -1
- package/dist/web-admin-client/facade.js +1 -1
- package/dist/web-admin-client/read-face.d.ts +73 -20
- package/dist/web-admin-client/read-face.js +76 -36
- package/dist/web-admin-client/skills-mcp.d.ts +1 -1
- package/dist/web-admin-client/skills-mcp.js +1 -1
- package/dist/web-admin-client/types.d.ts +3 -3
- package/package.json +3 -3
- package/dist/mcp-tool-axes.d.ts +0 -62
- 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 ??
|
|
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,
|
|
294
|
+
})), options?.abortSignal, first.ms);
|
|
284
295
|
}
|
|
285
296
|
catch (e) {
|
|
286
|
-
|
|
287
|
-
|
|
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 ? {
|
|
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,
|
|
423
|
+
})), options?.signal, first.ms);
|
|
406
424
|
}
|
|
407
425
|
catch (e) {
|
|
408
|
-
|
|
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 =
|
|
471
|
-
|
|
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 >=
|
|
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(
|
|
860
|
+
}, Math.max(bound.ms - (Date.now() - lastProgress()), 0));
|
|
818
861
|
};
|
|
819
|
-
|
|
820
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* `
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
* · **组合层**(恒有)——
|
|
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` 在场)——
|
|
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 {
|
|
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
|
|
806
|
-
const
|
|
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 =
|
|
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 (
|
|
919
|
+
if (liveness !== undefined && !finished && Date.now() - lastChunk >= liveness.ms) {
|
|
910
920
|
finished = true;
|
|
911
921
|
kill();
|
|
912
|
-
throw new RemoteExecutionError("timeout",
|
|
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
|
|
1327
|
-
|
|
1328
|
-
|
|
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
|
-
/**
|
|
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
|
|
555
|
-
*
|
|
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
|
|
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 ??
|
|
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
|
|
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
|
|
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
|
|
1131
|
-
const
|
|
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
|
|
1138
|
+
const liveness = bounds.liveness;
|
|
1134
1139
|
const armLiveness = () => {
|
|
1135
|
-
if (
|
|
1140
|
+
if (liveness === undefined)
|
|
1136
1141
|
return;
|
|
1137
1142
|
clearTimeout(livenessTimer);
|
|
1138
1143
|
const idle = Date.now() - lastProgress;
|
|
1139
|
-
if (idle >=
|
|
1140
|
-
finish({ ok: false, error: new ExecutionError("timeout",
|
|
1144
|
+
if (idle >= liveness.ms) {
|
|
1145
|
+
finish({ ok: false, error: new ExecutionError("timeout", livenessCutMessage(liveness)) });
|
|
1141
1146
|
return;
|
|
1142
1147
|
}
|
|
1143
|
-
livenessTimer = setTimeout(armLiveness,
|
|
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
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
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,
|
|
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,
|
|
1622
|
+
return this.runExec(command, { signal: abortSignal, capMs: this.cfg.dataTimeoutMs });
|
|
1632
1623
|
}
|
|
1633
1624
|
workingDirOf(cwd) {
|
|
1634
1625
|
const dir = cwd ?? this.cwd;
|