@mastra/deployer-sandbox 0.1.4-alpha.0 → 0.2.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.
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { a as SERVER_SCRIPT, c as launchServer, d as shellQuote, f as tailServerLog, i as SERVER_PIDFILE, l as resolveRemoteDir, n as INSTALL_MARKER, o as getInfoSafe, p as waitForHealthy, r as SERVER_LOGFILE, s as killPreviousServer, t as DEFAULT_PORT, u as runInSandbox } from "./shared-C-piQAcU.js";
1
+ import { a as SERVER_SCRIPT, c as launchServer, d as shellQuote, f as tailServerLog, i as SERVER_PIDFILE, l as resolveRemoteDir, n as INSTALL_MARKER$1, o as getInfoSafe, p as waitForHealthy, r as SERVER_LOGFILE, s as killPreviousServer, t as DEFAULT_PORT, u as runInSandbox } from "./shared-C-piQAcU.js";
2
2
  import { access, mkdtemp, readFile, rm, writeFile } from "fs/promises";
3
- import { dirname, join } from "path";
3
+ import { dirname, join, posix } from "path";
4
4
  import { fileURLToPath } from "url";
5
5
  import { Deployer } from "@mastra/deployer";
6
6
  import { copy } from "fs-extra/esm";
@@ -77,7 +77,7 @@ async function deployToSandbox(options) {
77
77
  await killPreviousServer(sandbox, remoteDir);
78
78
  await runInSandbox(sandbox, `cd ${shellQuote(remoteDir)} && tar -xzf .deploy.tgz && rm -f .deploy.tgz`, { timeout: 12e4 });
79
79
  const installHash = await hashInstallInputs(dir, installCommand);
80
- const marker = `${remoteDir}/${INSTALL_MARKER}`;
80
+ const marker = `${remoteDir}/${INSTALL_MARKER$1}`;
81
81
  const markerCheck = await runInSandbox(sandbox, `cat ${shellQuote(marker)} 2>/dev/null || true`, { allowFailure: true });
82
82
  if (installHash && markerCheck.stdout.trim() === installHash) logger.info("Dependencies unchanged — skipping install.");
83
83
  else {
@@ -379,6 +379,446 @@ var SandboxDeployer = class extends Deployer {
379
379
  }
380
380
  };
381
381
  //#endregion
382
- export { MANIFEST_FILENAME, SandboxDeployer, buildLaunchScript, deployToSandbox, readDeploymentManifest, updateEdgeConfigAlias, writeDeploymentManifest };
382
+ //#region src/worker.ts
383
+ const ARCHIVE = ".mastra-worker.tar.gz";
384
+ const RUNTIME_DIR = ".mastra/executions";
385
+ const INSTALL_MARKER = ".mastra-install-hash";
386
+ const INSTALL_LOCK = ".mastra-install-lock";
387
+ const ARTIFACT_LOCK = ".mastra-artifact-lock";
388
+ const EXECUTION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
389
+ const DEFAULT_INPUT_LIMIT = 16 * 1024 * 1024;
390
+ const DEFAULT_OUTPUT_READ_LIMIT = 1024 * 1024;
391
+ async function deployWorkerToSandbox(options) {
392
+ validateOptions(options);
393
+ const { sandbox, dir, executionId, command, mode = "worker", args = [], env = {}, workingDirectory = ".", installCommand = "npm install --omit=dev", startupTimeoutMs = 1e4, executionTimeoutMs, terminationGraceMs = 5e3, inputLimitBytes = DEFAULT_INPUT_LIMIT } = options;
394
+ const remoteDir = await resolveRemoteDir(sandbox, options.remoteDir);
395
+ const config = {
396
+ sandbox,
397
+ remoteDir,
398
+ command,
399
+ args,
400
+ env,
401
+ workingDirectory,
402
+ mode,
403
+ startupTimeoutMs,
404
+ executionTimeoutMs,
405
+ terminationGraceMs,
406
+ inputLimitBytes
407
+ };
408
+ const archive = `${remoteDir}/${ARCHIVE}`;
409
+ const tarball = await createTarball(dir);
410
+ const installHash = await hashInstallInputs(dir, installCommand);
411
+ const artifactLock = `${remoteDir}/${ARTIFACT_LOCK}`;
412
+ let artifactLockAcquired = false;
413
+ try {
414
+ await runInSandbox(sandbox, `mkdir -p ${shellQuote(remoteDir)}`);
415
+ await acquireLock(sandbox, artifactLock, options.installTimeoutMs, "worker artifact");
416
+ artifactLockAcquired = true;
417
+ await uploadFile(sandbox, archive, tarball);
418
+ await runInSandbox(sandbox, `tar -xzf ${shellQuote(archive)} -C ${shellQuote(remoteDir)} && rm -f ${shellQuote(archive)}`, { label: "extract worker artifact" });
419
+ } catch (error) {
420
+ throw workerPhaseError("upload", error);
421
+ } finally {
422
+ if (artifactLockAcquired) await runInSandbox(sandbox, `rm -rf ${shellQuote(artifactLock)}`, {
423
+ allowFailure: true,
424
+ label: "release worker artifact lock"
425
+ });
426
+ }
427
+ try {
428
+ await installDependencies(sandbox, remoteDir, installHash ?? void 0, installCommand, options.installTimeoutMs);
429
+ } catch (error) {
430
+ throw workerPhaseError("install", error);
431
+ }
432
+ return createExecution(config, executionId, options.input);
433
+ }
434
+ function validateOptions(options) {
435
+ if (!options.sandbox.executeCommand) throw new Error(`Sandbox provider "${options.sandbox.provider}" does not support executeCommand, which is required for worker deploys.`);
436
+ validateExecutionId(options.executionId);
437
+ if (!options.command || /[\0\r\n]/.test(options.command)) throw new Error("Worker command must be a non-empty executable path.");
438
+ if (options.args?.some((arg) => arg.includes("\0"))) throw new Error("Worker arguments must not contain NUL bytes.");
439
+ for (const key of Object.keys(options.env ?? {})) if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) throw new Error(`Invalid worker environment variable name: ${key}`);
440
+ validateRelativePath(options.workingDirectory ?? ".", "workingDirectory");
441
+ validateInput(options.input);
442
+ for (const [name, value] of [
443
+ ["inputLimitBytes", options.inputLimitBytes],
444
+ ["startupTimeoutMs", options.startupTimeoutMs],
445
+ ["executionTimeoutMs", options.executionTimeoutMs],
446
+ ["terminationGraceMs", options.terminationGraceMs]
447
+ ]) if (value !== void 0 && (!Number.isFinite(value) || value <= 0)) throw new Error(`${name} must be greater than zero.`);
448
+ }
449
+ function validateRelativePath(value, label) {
450
+ if (!value || posix.isAbsolute(value) || posix.normalize(value).startsWith("..")) throw new Error(`Worker ${label} must stay within the deployed artifact root.`);
451
+ }
452
+ function validateInput(input) {
453
+ if (input?.type === "file") validateRelativePath(input.path, "input file path");
454
+ }
455
+ async function acquireLock(sandbox, lock, timeout, label) {
456
+ const timeoutMs = timeout ?? 6e5;
457
+ const attempts = Math.max(1, Math.ceil(timeoutMs / 1e3));
458
+ await runInSandbox(sandbox, [
459
+ "i=0",
460
+ `while ! mkdir ${shellQuote(lock)} 2>/dev/null; do`,
461
+ ` if [ "$i" -ge ${attempts} ]; then echo ${shellQuote(`${label} lock timeout`)} >&2; exit 1; fi`,
462
+ " sleep 1; i=$((i + 1))",
463
+ "done"
464
+ ].join("\n"), {
465
+ timeout: timeoutMs,
466
+ label: `acquire ${label} lock`
467
+ });
468
+ }
469
+ async function installDependencies(sandbox, remoteDir, installHash, installCommand, timeout) {
470
+ if (!installHash) return;
471
+ const marker = `${remoteDir}/${INSTALL_MARKER}`;
472
+ const lock = `${remoteDir}/${INSTALL_LOCK}`;
473
+ await acquireLock(sandbox, lock, timeout, "dependency install");
474
+ try {
475
+ await runInSandbox(sandbox, [
476
+ `current="$(cat ${shellQuote(marker)} 2>/dev/null || true)"`,
477
+ `if [ "$current" != ${shellQuote(installHash)} ]; then`,
478
+ ` cd ${shellQuote(remoteDir)} && ${installCommand}`,
479
+ ` printf %s ${shellQuote(installHash)} > ${shellQuote(`${marker}.tmp`)}`,
480
+ ` mv ${shellQuote(`${marker}.tmp`)} ${shellQuote(marker)}`,
481
+ "fi"
482
+ ].join("\n"), {
483
+ timeout: timeout ?? 6e5,
484
+ label: "install worker dependencies"
485
+ });
486
+ } finally {
487
+ await runInSandbox(sandbox, `rm -rf ${shellQuote(lock)}`, {
488
+ allowFailure: true,
489
+ label: "release dependency install lock"
490
+ });
491
+ }
492
+ }
493
+ async function createExecution(config, executionId, input) {
494
+ validateExecutionId(executionId);
495
+ const paths = executionPaths(config.remoteDir, executionId);
496
+ await runInSandbox(config.sandbox, `mkdir -p ${shellQuote(`${config.remoteDir}/${RUNTIME_DIR}`)} && mkdir -m 700 ${shellQuote(paths.dir)}`, { label: "create worker execution namespace" });
497
+ const script = buildExecutionScript(config, paths, await stageInput(config, paths, input));
498
+ await uploadFile(config.sandbox, paths.script, Buffer.from(script));
499
+ await runInSandbox(config.sandbox, `chmod 700 ${shellQuote(paths.script)}`);
500
+ try {
501
+ await launchExecution(config.sandbox, paths);
502
+ } catch (error) {
503
+ await writeFailedStatus(config.sandbox, paths, executionId, "launch", error);
504
+ throw workerPhaseError("launch", error);
505
+ }
506
+ const startup = await waitForStartup(config, executionId, paths);
507
+ if (startup.state === "timed_out") await cancelExecution(config, executionId, paths, "startup");
508
+ if (startup.state === "failed" || startup.state === "timed_out" || startup.state === "provider_unavailable") throw new Error(`Worker ${startup.state} during startup${"message" in startup && startup.message ? `: ${startup.message}` : ""}.`);
509
+ const info = await getInfoSafe(config.sandbox);
510
+ return deployment(config, executionId, paths, info?.id ?? config.sandbox.id ?? "unknown", info?.timeoutAt);
511
+ }
512
+ function deployment(config, executionId, paths, sandboxId, expiresAt) {
513
+ return {
514
+ sandboxId,
515
+ executionId,
516
+ expiresAt,
517
+ status: (options) => readWorkerStatus(config.sandbox, executionId, paths, options),
518
+ readOutput: (stream, options) => readOutput(config.sandbox, executionId, paths, stream, options),
519
+ cancel: () => cancelExecution(config, executionId, paths),
520
+ stop: async () => {
521
+ if (!config.sandbox.stop) throw new Error(`Sandbox provider "${config.sandbox.provider}" does not support stop.`);
522
+ await config.sandbox.stop();
523
+ },
524
+ destroy: (options) => destroyWithRetry(config.sandbox, options),
525
+ relaunch: async (options) => {
526
+ if (options.executionId === executionId) throw new Error("Relaunch requires a new executionId.");
527
+ validateInput(options.input);
528
+ return createExecution(config, options.executionId, options.input);
529
+ }
530
+ };
531
+ }
532
+ async function stageInput(config, paths, input) {
533
+ if (!input) return void 0;
534
+ const data = typeof input.data === "string" ? Buffer.from(input.data) : Buffer.from(input.data);
535
+ if (data.byteLength > config.inputLimitBytes) throw new Error(`Worker input exceeds inputLimitBytes (${data.byteLength} > ${config.inputLimitBytes}).`);
536
+ const path = input.type === "stdin" ? paths.stdin : posix.resolve(config.remoteDir, input.path);
537
+ await uploadFile(config.sandbox, path, data);
538
+ await runInSandbox(config.sandbox, `chmod 600 ${shellQuote(path)}`);
539
+ return input.type === "stdin" ? path : void 0;
540
+ }
541
+ function buildExecutionScript(config, paths, stdinPath) {
542
+ const cwd = posix.resolve(config.remoteDir, config.workingDirectory);
543
+ const envPrefix = Object.entries(config.env).map(([key, value]) => `${key}=${shellQuote(value)}`).join(" ");
544
+ const executable = [shellQuote(config.command), ...config.args.map(shellQuote)].join(" ");
545
+ const target = `${envPrefix ? `env ${envPrefix} ` : ""}${executable}`;
546
+ const graceAttempts = Math.max(1, Math.ceil(config.terminationGraceMs / 1e3));
547
+ const state = (value) => `tmp=${shellQuote(`${paths.status}.tmp.$$`)}; printf '%s\\n' ${shellQuote(value)} > "$tmp"; mv "$tmp" ${shellQuote(paths.status)};`;
548
+ return [
549
+ "#!/bin/sh",
550
+ `cd ${shellQuote(cwd)}`,
551
+ `execution_id=${shellQuote(paths.executionId)}`,
552
+ `stdout=${shellQuote(paths.stdout)}`,
553
+ `stderr=${shellQuote(paths.stderr)}`,
554
+ `pidfile=${shellQuote(paths.pid)}`,
555
+ `tokenfile=${shellQuote(paths.pidToken)}`,
556
+ `: > "$stdout"; : > "$stderr"`,
557
+ state(`starting|${paths.executionId}`),
558
+ `setsid sh -c ${shellQuote(`exec ${target}${stdinPath ? ` < ${shellQuote(stdinPath)}` : ""}`)} > "$stdout" 2> "$stderr" &`,
559
+ "child=$!",
560
+ "printf %s \"$child\" > \"$pidfile\"",
561
+ `if [ -r "/proc/$child/stat" ]; then awk '{print $22}' "/proc/$child/stat" > "$tokenfile"; else : > "$tokenfile"; fi`,
562
+ state(`running|${paths.executionId}`),
563
+ "cancelled=0",
564
+ `trap 'cancelled=1; kill -TERM -"$child" 2>/dev/null || kill -TERM "$child" 2>/dev/null || true' TERM INT`,
565
+ ...config.executionTimeoutMs ? [`(sleep ${Math.max(1, Math.ceil(config.executionTimeoutMs / 1e3))}; if kill -0 "$child" 2>/dev/null; then ${state(`timed_out|${paths.executionId}|execution`)} kill -TERM -"$child" 2>/dev/null || kill -TERM "$child" 2>/dev/null || true; i=0; while kill -0 "$child" 2>/dev/null && [ "$i" -lt ${graceAttempts} ]; do sleep 1; i=$((i + 1)); done; kill -KILL -"$child" 2>/dev/null || kill -KILL "$child" 2>/dev/null || true; fi) &`, "watchdog=$!"] : [],
566
+ "wait \"$child\"",
567
+ "code=$?",
568
+ ...config.executionTimeoutMs ? ["kill \"$watchdog\" 2>/dev/null || true"] : [],
569
+ `current="$(cat ${shellQuote(paths.status)} 2>/dev/null || true)"`,
570
+ `case "$current" in timed_out*) ;; *) if [ "$cancelled" -eq 1 ]; then ${state(`cancelled|${paths.executionId}|TERM`)} else signal=''; if [ "$code" -gt 128 ]; then signal="SIG$((code - 128))"; fi; tmp=${shellQuote(`${paths.status}.tmp.$$`)}; printf 'exited|%s|%s|%s\\n' "$execution_id" "$code" "$signal" > "$tmp"; mv "$tmp" ${shellQuote(paths.status)}; fi ;; esac`,
571
+ "rm -f \"$pidfile\" \"$tokenfile\"",
572
+ "exit \"$code\""
573
+ ].join("\n");
574
+ }
575
+ async function launchExecution(sandbox, paths) {
576
+ await runInSandbox(sandbox, `setsid nohup sh ${shellQuote(paths.script)} >/dev/null 2>&1 & echo $!`, { label: "launch worker execution" });
577
+ }
578
+ async function waitForStartup(config, executionId, paths) {
579
+ const deadline = Date.now() + config.startupTimeoutMs;
580
+ while (Date.now() < deadline) {
581
+ const status = await readWorkerStatus(config.sandbox, executionId, paths);
582
+ if (status.state !== "unknown" && status.state !== "starting") return status;
583
+ await new Promise((resolve) => setTimeout(resolve, 100));
584
+ }
585
+ return {
586
+ state: "timed_out",
587
+ executionId,
588
+ phase: "startup"
589
+ };
590
+ }
591
+ async function readWorkerStatus(sandbox, executionId, paths, options) {
592
+ const providerState = sandbox.status;
593
+ if (providerState === "destroyed" || providerState === "destroying") return {
594
+ state: "provider_unavailable",
595
+ executionId,
596
+ providerState
597
+ };
598
+ if (providerState === "stopped" || providerState === "stopping") {
599
+ if (!options?.wake || !sandbox.start) return {
600
+ state: "provider_unavailable",
601
+ executionId,
602
+ providerState
603
+ };
604
+ try {
605
+ await sandbox.start();
606
+ } catch (error) {
607
+ return {
608
+ state: "provider_unavailable",
609
+ executionId,
610
+ providerState,
611
+ message: errorMessage(error)
612
+ };
613
+ }
614
+ }
615
+ try {
616
+ const result = await runInSandbox(sandbox, [
617
+ `status="$(cat ${shellQuote(paths.status)} 2>/dev/null || true)"`,
618
+ `if [ -f ${shellQuote(paths.pid)} ]; then`,
619
+ ` pid="$(cat ${shellQuote(paths.pid)})"`,
620
+ ` expected="$(cat ${shellQuote(paths.pidToken)} 2>/dev/null || true)"`,
621
+ ` actual="$(if [ -r "/proc/$pid/stat" ]; then awk '{print $22}' "/proc/$pid/stat"; fi)"`,
622
+ ` if kill -0 "$pid" 2>/dev/null && { [ -z "$expected" ] || [ "$expected" = "$actual" ]; }; then echo "running|${executionId}"; exit 0; fi`,
623
+ ` if kill -0 "$pid" 2>/dev/null; then echo "stale|${executionId}"; exit 0; fi`,
624
+ "fi",
625
+ `if [ -n "$status" ]; then printf '%s\\n' "$status"; else echo "unknown|${executionId}"; fi`
626
+ ].join("\n"), {
627
+ allowFailure: true,
628
+ label: "read worker status"
629
+ });
630
+ if (result.exitCode !== 0) return {
631
+ state: "provider_unavailable",
632
+ executionId,
633
+ providerState: sandbox.status,
634
+ message: result.stderr || result.stdout || "Sandbox status inspection failed."
635
+ };
636
+ return parseStatus(executionId, result.stdout.trim());
637
+ } catch (error) {
638
+ return {
639
+ state: "provider_unavailable",
640
+ executionId,
641
+ providerState: sandbox.status,
642
+ message: errorMessage(error)
643
+ };
644
+ }
645
+ }
646
+ function parseStatus(executionId, value) {
647
+ const [state, recordedId, first, second] = value.split("|");
648
+ if (recordedId !== executionId || state === "stale") return {
649
+ state: "unknown",
650
+ executionId
651
+ };
652
+ if (state === "starting") return {
653
+ state,
654
+ executionId
655
+ };
656
+ if (state === "running") return {
657
+ state,
658
+ executionId
659
+ };
660
+ if (state === "exited") {
661
+ const exitCode = Number(first);
662
+ return Number.isInteger(exitCode) ? {
663
+ state,
664
+ executionId,
665
+ exitCode,
666
+ ...second ? { signal: second } : {}
667
+ } : {
668
+ state: "unknown",
669
+ executionId
670
+ };
671
+ }
672
+ if (state === "cancelled") return {
673
+ state,
674
+ executionId,
675
+ ...first ? { signal: first } : {}
676
+ };
677
+ if (state === "timed_out" && (first === "startup" || first === "execution")) return {
678
+ state,
679
+ executionId,
680
+ phase: first
681
+ };
682
+ if (state === "failed" && (first === "upload" || first === "install" || first === "launch")) return {
683
+ state,
684
+ executionId,
685
+ phase: first,
686
+ message: second ?? ""
687
+ };
688
+ return {
689
+ state: "unknown",
690
+ executionId
691
+ };
692
+ }
693
+ async function cancelExecution(config, executionId, paths, timeoutPhase) {
694
+ const current = await readWorkerStatus(config.sandbox, executionId, paths);
695
+ if (current.state !== "running" && current.state !== "starting") return current;
696
+ const attempts = Math.max(1, Math.ceil(config.terminationGraceMs / 1e3));
697
+ const terminal = timeoutPhase ? `timed_out|${executionId}|startup` : `cancelled|${executionId}|TERM`;
698
+ await runInSandbox(config.sandbox, [
699
+ `pid="$(cat ${shellQuote(paths.pid)} 2>/dev/null || true)"`,
700
+ "[ -n \"$pid\" ] || exit 0",
701
+ `expected="$(cat ${shellQuote(paths.pidToken)} 2>/dev/null || true)"`,
702
+ `actual="$(if [ -r "/proc/$pid/stat" ]; then awk '{print $22}' "/proc/$pid/stat"; fi)"`,
703
+ "[ -n \"$expected\" ] && [ \"$expected\" != \"$actual\" ] && exit 0",
704
+ "kill -TERM -\"$pid\" 2>/dev/null || kill -TERM \"$pid\" 2>/dev/null || true",
705
+ `i=0; while kill -0 "$pid" 2>/dev/null && [ "$i" -lt ${attempts} ]; do sleep 1; i=$((i + 1)); done`,
706
+ "kill -KILL -\"$pid\" 2>/dev/null || kill -KILL \"$pid\" 2>/dev/null || true",
707
+ `tmp=${shellQuote(`${paths.status}.tmp.$$`)}; printf '%s\\n' ${shellQuote(terminal)} > "$tmp"; mv "$tmp" ${shellQuote(paths.status)}`,
708
+ `rm -f ${shellQuote(paths.pid)} ${shellQuote(paths.pidToken)}`
709
+ ].join("\n"), {
710
+ allowFailure: true,
711
+ timeout: config.terminationGraceMs + 5e3,
712
+ label: "cancel worker execution"
713
+ });
714
+ return parseStatus(executionId, terminal);
715
+ }
716
+ async function readOutput(sandbox, executionId, paths, stream, options) {
717
+ const offset = Math.max(0, Math.floor(options?.offset ?? 0));
718
+ const maxBytes = Math.max(1, Math.floor(options?.maxBytes ?? DEFAULT_OUTPUT_READ_LIMIT));
719
+ const path = stream === "stdout" ? paths.stdout : paths.stderr;
720
+ try {
721
+ const result = await runInSandbox(sandbox, `size=$(wc -c < ${shellQuote(path)} 2>/dev/null || echo 0); printf '%s\\n' "$size"; tail -c +${offset + 1} ${shellQuote(path)} 2>/dev/null | head -c ${maxBytes} | base64`, {
722
+ allowFailure: true,
723
+ label: `read worker ${stream}`
724
+ });
725
+ if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `Unable to read worker ${stream}.`);
726
+ const newline = result.stdout.indexOf("\n");
727
+ const totalBytes = Number((newline === -1 ? result.stdout : result.stdout.slice(0, newline)).trim()) || 0;
728
+ const encoded = newline === -1 ? "" : result.stdout.slice(newline + 1).replace(/\s/g, "");
729
+ const data = Buffer.from(encoded, "base64");
730
+ const nextOffset = offset + data.byteLength;
731
+ const status = await readWorkerStatus(sandbox, executionId, paths);
732
+ const terminal = [
733
+ "exited",
734
+ "cancelled",
735
+ "timed_out",
736
+ "failed"
737
+ ].includes(status.state);
738
+ const interrupted = status.state === "provider_unavailable" || status.state === "unknown";
739
+ return {
740
+ stream,
741
+ data,
742
+ offset,
743
+ nextOffset,
744
+ totalBytes,
745
+ eof: terminal && nextOffset >= totalBytes,
746
+ truncated: nextOffset < totalBytes,
747
+ interrupted
748
+ };
749
+ } catch {
750
+ return {
751
+ stream,
752
+ data: /* @__PURE__ */ new Uint8Array(),
753
+ offset,
754
+ nextOffset: offset,
755
+ totalBytes: offset,
756
+ eof: false,
757
+ truncated: false,
758
+ interrupted: true
759
+ };
760
+ }
761
+ }
762
+ async function destroyWithRetry(sandbox, options) {
763
+ if (!sandbox.destroy) return {
764
+ state: "unsupported",
765
+ attempts: 0
766
+ };
767
+ const attempts = Math.max(1, Math.floor(options?.attempts ?? 3));
768
+ const delayMs = Math.max(0, Math.floor(options?.delayMs ?? 250));
769
+ let lastError;
770
+ for (let attempt = 1; attempt <= attempts; attempt++) try {
771
+ await sandbox.destroy();
772
+ return {
773
+ state: "destroyed",
774
+ attempts: attempt
775
+ };
776
+ } catch (error) {
777
+ lastError = error;
778
+ if (attempt < attempts) await new Promise((resolve) => setTimeout(resolve, delayMs));
779
+ }
780
+ return {
781
+ state: "exhausted",
782
+ attempts,
783
+ error: lastError
784
+ };
785
+ }
786
+ async function writeFailedStatus(sandbox, paths, executionId, phase, error) {
787
+ await writeStatus(sandbox, paths.status, `failed|${executionId}|${phase}|${sanitizeStatusValue(errorMessage(error))}`);
788
+ }
789
+ async function writeStatus(sandbox, path, value) {
790
+ await runInSandbox(sandbox, `tmp=${shellQuote(`${path}.tmp.$$`)}; printf '%s\\n' ${shellQuote(value)} > "$tmp"; mv "$tmp" ${shellQuote(path)}`, {
791
+ allowFailure: true,
792
+ label: "write worker status"
793
+ });
794
+ }
795
+ function executionPaths(remoteDir, executionId) {
796
+ const dir = `${remoteDir}/${RUNTIME_DIR}/${executionId}`;
797
+ return {
798
+ executionId,
799
+ dir,
800
+ script: `${dir}/launch.sh`,
801
+ pid: `${dir}/pid`,
802
+ pidToken: `${dir}/pid-start`,
803
+ status: `${dir}/status`,
804
+ stdin: `${dir}/stdin`,
805
+ stdout: `${dir}/stdout`,
806
+ stderr: `${dir}/stderr`
807
+ };
808
+ }
809
+ function validateExecutionId(executionId) {
810
+ if (!executionId || !EXECUTION_ID_PATTERN.test(executionId)) throw new Error("Worker executionId must contain only letters, numbers, dots, underscores, and hyphens.");
811
+ }
812
+ function workerPhaseError(phase, error) {
813
+ return new Error(`Worker ${phase} failed: ${errorMessage(error)}`, { cause: error });
814
+ }
815
+ function errorMessage(error) {
816
+ return error instanceof Error ? error.message : String(error);
817
+ }
818
+ function sanitizeStatusValue(value) {
819
+ return value.replace(/[|\r\n]/g, " ").slice(0, 500);
820
+ }
821
+ //#endregion
822
+ export { MANIFEST_FILENAME, SandboxDeployer, buildLaunchScript, deployToSandbox, deployWorkerToSandbox, readDeploymentManifest, updateEdgeConfigAlias, writeDeploymentManifest };
383
823
 
384
824
  //# sourceMappingURL=index.js.map