@claxedo/sandbox-manager 0.10.0 → 0.10.2

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.
@@ -49,6 +49,13 @@ var SandboxRuntimeBootError = class extends Error {
49
49
  this.name = "SandboxRuntimeBootError";
50
50
  }
51
51
  };
52
+ var SandboxImageDownloadError = class extends Error {
53
+ imageDownloadFailed = true;
54
+ constructor(reason) {
55
+ super(reason);
56
+ this.name = "SandboxImageDownloadError";
57
+ }
58
+ };
52
59
  var SandboxHostGoneError = class extends Error {
53
60
  hostGone = true;
54
61
  constructor(reason) {
@@ -149,6 +156,13 @@ function envFile(input) {
149
156
  }).join("\n") + "\n";
150
157
  }
151
158
 
159
+ // src/runtime-version.ts
160
+ var SERVED_WORKSPACE_RUNTIME_FLOOR = {
161
+ version: "0.10.1",
162
+ image: "ghcr.io/kyashrathore/claxedo-sandbox:workspace-runtime-0-10-1-40706b7356-agent-plugins-v8"
163
+ };
164
+ var MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION = SERVED_WORKSPACE_RUNTIME_FLOOR.version;
165
+
152
166
  // src/image-name.ts
153
167
  function assertSandboxImageReference(image) {
154
168
  if (!image || image !== image.trim() || image.startsWith("-") || /\s/.test(image)) {
@@ -560,6 +574,98 @@ function isTransientDriverError(err, markers) {
560
574
  }
561
575
  var TRANSIENT_DRIVER_RETRY_MS = 2e3;
562
576
 
577
+ // src/drivers/boat-machine.ts
578
+ var IMAGE_PREPARATION_TIMEOUT_SECONDS = 600;
579
+ var CONTAINER_START_TIMEOUT_SECONDS = 120;
580
+ var DEFAULT_CONTAINER_NAME = "claxedo-runtime";
581
+ var DOCKER_DAEMON_WAIT_SECONDS = 120;
582
+ var RUNTIME_ENV_PATH = ".claxedo-runtime-env";
583
+ var REGISTRY_PASSWORD_PATH = ".claxedo-registry-password";
584
+ var BOOT_LOCK_PATH = "/run/lock/claxedo-boot.lock";
585
+ var BOOT_LOCK_HEADROOM_SECONDS = 60;
586
+ var CONTAINER_ENV_PATH = "/run/claxedo-runtime.env";
587
+ var PERSISTENT_ROOT = "claxedo-persistent";
588
+ var PERSISTENT_HOME_MOUNTS = [["claxedo", "/root/.claxedo"], ["workspace-runtime", "/root/.workspace-runtime"]];
589
+ var IMAGE_TREES = ["root", "usr", "etc", "var", "opt", "home", "srv"];
590
+ var IMAGE_TREE_SEED_EXCLUDES = ["root/.cache", "root/.npm/_cacache"];
591
+ var IMAGE_TREES_COMPLETE_MARKER = ".complete";
592
+ var SEED_CONTAINER_NAME = "claxedo-seed";
593
+ var STUB_IMAGE = "claxedo-runtime-stub";
594
+ var STUB_IMAGE_ARCHIVE = "stub.tar";
595
+ var IMAGE_LABEL = "claxedo.image";
596
+ var IMAGE_ENV_FILE = "image.env";
597
+ var ROOT_LINKS = ["bin", "lib", "lib64", "sbin"];
598
+ var ROOT_DIRECTORIES = ["boot", "dev", "media", "mnt", "proc", "run", "sys", "tmp", ...IMAGE_TREES];
599
+ var BOAT_SAVED_IMAGES = "/var/lib/ascii-docker-images";
600
+ var DOCKER_NAME_IN_USE = "is already in use by container";
601
+ var CONTAINER_CREATE_ATTEMPTS = 5;
602
+ var CONTAINER_SECURITY_OPTIONS = "--security-opt seccomp=unconfined --security-opt apparmor=unconfined";
603
+ var VM_DOCKER_INIT = "/usr/libexec/docker/docker-init";
604
+ var CONTAINER_INIT_PATH = "/claxedo-init";
605
+ function holdingBootLock(commandTimeoutSeconds, script) {
606
+ const wait = commandTimeoutSeconds - BOOT_LOCK_HEADROOM_SECONDS;
607
+ return `exec 9>>${BOOT_LOCK_PATH} && { flock -w ${wait} 9 || { echo "another boot of this machine held ${BOOT_LOCK_PATH} for ${wait} s" >&2; exit 1; }; } && ${script}`;
608
+ }
609
+ function containerHostname(workspaceId) {
610
+ return workspaceId.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "").slice(0, 63) || "claxedo";
611
+ }
612
+ function bootedImageFile(staged) {
613
+ return `${staged.env}.image`;
614
+ }
615
+ function bootedImage(prepareStdout) {
616
+ return prepareStdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).pop();
617
+ }
618
+ var VERSION_FILE = "opt/workspace-runtime/workspace-runtime-version";
619
+ var stubLabel = `docker image inspect --format '{{index .Config.Labels "${IMAGE_LABEL}"}}' ${STUB_IMAGE} 2>/dev/null`;
620
+ var treesFunctions = [
621
+ `trees_of() { printf '%s/${PERSISTENT_ROOT}/image-%s' "$(pwd)" "$(printf %s "$1" | sed 's/[^A-Za-z0-9._-]/-/g')"; }`,
622
+ `seeded() { [ -f "$1/${IMAGE_TREES_COMPLETE_MARKER}" ] && [ -f "$1/${STUB_IMAGE_ARCHIVE}" ] && [ -f "$1/${IMAGE_ENV_FILE}" ]; }`,
623
+ `served() { version=$(cat "$1/${VERSION_FILE}" 2>/dev/null) && [ -n "$version" ] && [ "$(printf '%s\\n%s\\n' ${MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION} "$version" | sort -V | head -n 1)" = ${MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION} ]; }`
624
+ ].join(" && ");
625
+ var IMAGE_DOWNLOAD_INTERRUPTED = "image_download_interrupted";
626
+ var KEPT_IMAGE_SKIPPED = "kept_image_skipped:";
627
+ var PULL_RESUMING = `pull_resuming() { finished=$(mktemp) && while :; do if output=$(docker pull "$1" 2>&1); then rm -f "$finished"; return 0; fi; case "$output" in *"connection reset by peer"*|*"unexpected EOF"*|*"i/o timeout"*|*"TLS handshake timeout"*) ;; *) printf '%s\\n' "$output" >&2; rm -f "$finished"; return 1 ;; esac; before=$(wc -l < "$finished"); { cat "$finished"; printf '%s\\n' "$output" | sed -n -e 's/^\\([0-9a-f]\\{12\\}\\): Download complete$/\\1/p' -e 's/^\\([0-9a-f]\\{12\\}\\): Pull complete$/\\1/p'; } | sort -u > "$finished.next" && mv "$finished.next" "$finished"; [ "$(wc -l < "$finished")" -gt "$before" ] || { printf '%s\\n' "$output" ${IMAGE_DOWNLOAD_INTERRUPTED} >&2; rm -f "$finished"; return 1; }; done; }`;
628
+ function machineBootScripts(boot) {
629
+ const named = shell(boot.image);
630
+ const trees = `"$(trees_of ${named})"`;
631
+ const bootedFile = bootedImageFile(boot.staged);
632
+ const containerOfBooted = `[ "$(docker container inspect --format '{{index .Config.Labels "${IMAGE_LABEL}"}}' ${boot.containerName} 2>/dev/null)" = "$booted" ]`;
633
+ const bootScript = [
634
+ `. ${CONTAINER_ENV_PATH}`,
635
+ `mkdir -p ${shell(boot.directory)}`,
636
+ `cd ${shell(boot.directory)}`,
637
+ `exec ${boot.runtimeCommand}`
638
+ ].join(" && ");
639
+ const mounts = [["workspace", boot.directory], ...PERSISTENT_HOME_MOUNTS];
640
+ const run = `docker run -d --name ${boot.containerName} --hostname ${shell(containerHostname(boot.workspaceId))} -p ${boot.port}:${boot.port} -v ${VM_DOCKER_INIT}:${CONTAINER_INIT_PATH}:ro -v "$(pwd)/${RUNTIME_ENV_PATH}:${CONTAINER_ENV_PATH}:ro" --env-file "$trees/${IMAGE_ENV_FILE}" ` + IMAGE_TREES.map((tree) => `-v "$trees/${tree}":/${tree} `).join("") + mounts.map(([source, target]) => `-v "$(pwd)/${PERSISTENT_ROOT}/${source}":${shell(target)} `).join("") + `${CONTAINER_SECURITY_OPTIONS} --entrypoint ${CONTAINER_INIT_PATH} ${STUB_IMAGE} -- sh -lc ${shell(bootScript)}`;
641
+ const seedTrees = `sudo rm -rf ${trees} && sudo mkdir -p ${trees} && { docker rm -f ${SEED_CONTAINER_NAME} >/dev/null 2>&1 || true; } && docker create --name ${SEED_CONTAINER_NAME} ${named} true >/dev/null && for tree in ${IMAGE_TREES.join(" ")}; do docker cp ${SEED_CONTAINER_NAME}:/$tree - | sudo tar -xf - -C ${trees} ${IMAGE_TREE_SEED_EXCLUDES.map((exclude) => `--exclude=${exclude}`).join(" ")} && [ -d ${trees}/$tree ] || exit 1; done && docker rm ${SEED_CONTAINER_NAME} >/dev/null && sudo touch ${trees}/${IMAGE_TREES_COMPLETE_MARKER}`;
642
+ const seedStub = `stub=$(mktemp -d) && docker image inspect --format '{{range .Config.Env}}{{println .}}{{end}}' ${named} > "$stub/${IMAGE_ENV_FILE}" && workdir=$(docker image inspect --format '{{or .Config.WorkingDir "/"}}' ${named}) && mkdir "$stub/root" ${ROOT_DIRECTORIES.map((directory) => `"$stub/root/${directory}"`).join(" ")} && ${ROOT_LINKS.map((link) => `ln -s usr/${link} "$stub/root/${link}"`).join(" && ")} && chmod 1777 "$stub/root/tmp" && tar --owner=0 --group=0 --numeric-owner -cf "$stub/skeleton.tar" -C "$stub/root" . && docker import --change "WORKDIR $workdir" --change ${shell(`LABEL ${IMAGE_LABEL}=${boot.image}`)} "$stub/skeleton.tar" ${STUB_IMAGE} >/dev/null && docker save -o "$stub/${STUB_IMAGE_ARCHIVE}" ${STUB_IMAGE} && sudo mv "$stub/${IMAGE_ENV_FILE}" "$stub/${STUB_IMAGE_ARCHIVE}" ${trees}/ && rm -rf "$stub"`;
643
+ const seedNamed = `{ docker image inspect ${named} >/dev/null 2>&1 || pull_resuming ${named}; } && { [ -f ${trees}/${IMAGE_TREES_COMPLETE_MARKER} ] || { ${seedTrees}; }; } && ${seedStub}`;
644
+ const keepPrevious = `previous=$(${stubLabel}) && [ -n "$previous" ] && seeded "$(trees_of "$previous")" && served "$(trees_of "$previous")"`;
645
+ const keptImageSkipped = `echo "${KEPT_IMAGE_SKIPPED} stub=\${previous:-none} seeded=$(seeded "$(trees_of "$previous")" && echo yes || echo no) version=$(cat "$(trees_of "$previous")/${VERSION_FILE}" 2>/dev/null || echo none)" >&2`;
646
+ const removeImage = `if docker image inspect "$booted" >/dev/null 2>&1; then leftovers=$(docker ps -aq --filter ancestor="$booted") && { [ -z "$leftovers" ] || docker rm -f $leftovers >/dev/null; } && id=$(docker image inspect --format '{{.Id}}' "$booted") && docker rmi "$booted" >/dev/null && sudo rm -f ${BOAT_SAVED_IMAGES}/"\${id#sha256:}".tar; fi`;
647
+ const prepare = [
648
+ `chmod 600 ${boot.staged.env}`,
649
+ `find . -maxdepth 1 \\( -name '${RUNTIME_ENV_PATH}.*' -o -name '${REGISTRY_PASSWORD_PATH}.*' \\) -mmin +${boot.staleStagedMinutes} -exec rm -f {} +`,
650
+ `timeout ${DOCKER_DAEMON_WAIT_SECONDS} sh -c 'until docker info >/dev/null 2>&1; do sleep 1; done'`,
651
+ `[ -x ${VM_DOCKER_INIT} ]`,
652
+ ...boot.registry && boot.staged.password ? [`{ chmod 600 ${boot.staged.password}; docker login ${shell(boot.registry.server)} --username ${shell(boot.registry.username)} --password-stdin < ${boot.staged.password}; rc=$?; rm -f ${boot.staged.password}; (exit $rc); }`] : [],
653
+ treesFunctions,
654
+ PULL_RESUMING,
655
+ `booted=${named}`,
656
+ `if ! seeded ${trees}; then ` + (boot.keepPreviousImage ? `if ${keepPrevious}; then booted=$previous; else ${keptImageSkipped}; ${seedNamed}; fi` : seedNamed) + `; fi`,
657
+ `{ [ "$(${stubLabel})" = "$booted" ] || docker load -i "$(trees_of "$booted")/${STUB_IMAGE_ARCHIVE}" >/dev/null; }`,
658
+ `sudo find "$(pwd)/${PERSISTENT_ROOT}" -mindepth 1 -maxdepth 1 -name 'image-*' ! -path "$(trees_of "$booted")" -exec rm -rf {} +`,
659
+ `printf '%s\\n' "$booted" > ${bootedFile}`,
660
+ `printf '%s\\n' "$booted"`
661
+ ];
662
+ const start = `${treesFunctions} && booted=$(cat ${bootedFile}) && trees=$(trees_of "$booted") && rm -f ${bootedFile} && mv -f ${boot.staged.env} ${RUNTIME_ENV_PATH} && up=; for attempt in $(seq ${CONTAINER_CREATE_ATTEMPTS}); do [ "$attempt" = 1 ] || sleep 1; if ${containerOfBooted}; then failure=$(docker start ${boot.containerName} 2>&1 >/dev/null) && { up=1; break; }; else docker rm -f ${boot.containerName} >/dev/null 2>&1; failure=$(${run} 2>&1 >/dev/null) && { up=1; break; }; fi; case "$failure" in *${shell(DOCKER_NAME_IN_USE)}*) ;; *) break ;; esac; done; [ -n "$up" ] || { echo "$failure" >&2; false; } && ${removeImage}`;
663
+ return {
664
+ prepare: holdingBootLock(IMAGE_PREPARATION_TIMEOUT_SECONDS, prepare.join(" && ")),
665
+ start: holdingBootLock(CONTAINER_START_TIMEOUT_SECONDS, start)
666
+ };
667
+ }
668
+
563
669
  // src/drivers/boat.ts
564
670
  function formatBoatRuntimeTemplate(template) {
565
671
  return JSON.stringify({ image: template.image, shareCode: template.shareCode });
@@ -579,7 +685,6 @@ function parseBoatRuntimeTemplate(setting) {
579
685
  }
580
686
  var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
581
687
  var DEFAULT_WORKSPACE_DIR = "/workspace";
582
- var DEFAULT_CONTAINER_NAME = "claxedo-runtime";
583
688
  var DEFAULT_PROVISION_TIMEOUT_MS = 12e4;
584
689
  var DEFAULT_STATE_POLL_MS = { firstMs: 250, maxMs: 2e3 };
585
690
  var DEFAULT_HEALTH_TIMEOUT_MS = 10 * 6e4;
@@ -587,35 +692,8 @@ var DEFAULT_HEALTH_INTERVAL_MS = 100;
587
692
  var HEALTH_PROBE_WINDOW_SECONDS = 50;
588
693
  var HEALTH_PROBE_MARGIN_SECONDS = 10;
589
694
  var BOOTING_CONTAINER_STATES = /* @__PURE__ */ new Set(["running", "paused"]);
590
- var IMAGE_PREPARATION_TIMEOUT_SECONDS = 600;
591
- var CONTAINER_START_TIMEOUT_SECONDS = 120;
592
695
  var HOST_PUBLISH_TIMEOUT_SECONDS = 120;
593
696
  var BOOT_TTL_SECONDS = 30 * 60;
594
- var DOCKER_DAEMON_WAIT_SECONDS = 120;
595
- var RUNTIME_ENV_PATH = ".claxedo-runtime-env";
596
- var REGISTRY_PASSWORD_PATH = ".claxedo-registry-password";
597
- var BOOT_LOCK_PATH = "/run/lock/claxedo-boot.lock";
598
- var BOOT_LOCK_HEADROOM_SECONDS = 60;
599
- var CONTAINER_ENV_PATH = "/run/claxedo-runtime.env";
600
- var PERSISTENT_ROOT = "claxedo-persistent";
601
- var PERSISTENT_HOME_MOUNTS = [["claxedo", "/root/.claxedo"], ["workspace-runtime", "/root/.workspace-runtime"]];
602
- var IMAGE_TREES = ["root", "usr", "etc", "var", "opt", "home", "srv"];
603
- var IMAGE_TREE_SEED_EXCLUDES = ["root/.cache", "root/.npm/_cacache"];
604
- var IMAGE_TREES_COMPLETE_MARKER = ".complete";
605
- var SEED_CONTAINER_NAME = "claxedo-seed";
606
- var STUB_IMAGE = "claxedo-runtime-stub";
607
- var STUB_IMAGE_ARCHIVE = "stub.tar";
608
- var IMAGE_LABEL = "claxedo.image";
609
- var IMAGE_ENV_FILE = "image.env";
610
- var ROOT_LINKS = ["bin", "lib", "lib64", "sbin"];
611
- var ROOT_DIRECTORIES = ["boot", "dev", "media", "mnt", "proc", "run", "sys", "tmp", ...IMAGE_TREES];
612
- var BOAT_SAVED_IMAGES = "/var/lib/ascii-docker-images";
613
- var BOAT_CONTAINER_RECORD = "/etc/ascii-docker.json";
614
- var DOCKER_NAME_IN_USE = "is already in use by container";
615
- var CONTAINER_CREATE_ATTEMPTS = 5;
616
- var CONTAINER_SECURITY_OPTIONS = "--security-opt seccomp=unconfined --security-opt apparmor=unconfined";
617
- var VM_DOCKER_INIT = "/usr/libexec/docker/docker-init";
618
- var CONTAINER_INIT_PATH = "/claxedo-init";
619
697
  var CONTAINER_STOP_GRACE_SECONDS = 30;
620
698
  var HOST_RELAY_NOT_STOPPED = "The sandbox could not stop the localhost relay of this port. Try again.";
621
699
  var STDERR_TAIL_CHARS = 600;
@@ -627,16 +705,6 @@ var GONE_CODES = /* @__PURE__ */ new Set(["not_found", "snapshot_data_expired"])
627
705
  var RESUME_UNDER_WAY = "sandbox_resuming";
628
706
  var NOT_RUNNING_CODES = /* @__PURE__ */ new Set(["machine_not_running", "sandbox_not_ready"]);
629
707
  var TEMPLATE_UNREADABLE_CODES = /* @__PURE__ */ new Set(["snapshot_not_found", "api_key_action_forbidden", "api_key_sandbox_forbidden"]);
630
- function boatTemplateResetCommand() {
631
- const bootFiles = [
632
- `${PERSISTENT_ROOT}/workspace`,
633
- ...PERSISTENT_HOME_MOUNTS.map(([source]) => `${PERSISTENT_ROOT}/${source}`),
634
- RUNTIME_ENV_PATH,
635
- `${RUNTIME_ENV_PATH}.*`,
636
- `${REGISTRY_PASSWORD_PATH}.*`
637
- ];
638
- return `{ docker rm -f ${DEFAULT_CONTAINER_NAME} >/dev/null 2>&1 || true; } && sudo rm -rf ${bootFiles.join(" ")} && [ -z "$(docker ps -aq)" ] && echo '[]' | sudo tee ${BOAT_CONTAINER_RECORD} >/dev/null && sudo find ${BOAT_SAVED_IMAGES} -mindepth 1 -delete && ls -d ${PERSISTENT_ROOT}/image-*/${IMAGE_TREES_COMPLETE_MARKER}`;
639
- }
640
708
  function hostGoneOrThrow(err) {
641
709
  if (err instanceof BoatApiError && GONE_CODES.has(err.code)) throw new SandboxHostGoneError(err.message);
642
710
  throw err;
@@ -649,10 +717,6 @@ function archivedOrThrow(err) {
649
717
  if (err instanceof BoatApiError && err.code === "stop_failed" && err.sandboxState === "archived") return;
650
718
  hostGoneOrThrow(err);
651
719
  }
652
- function holdingBootLock(commandTimeoutSeconds, script) {
653
- const wait = commandTimeoutSeconds - BOOT_LOCK_HEADROOM_SECONDS;
654
- return `exec 9>>${BOOT_LOCK_PATH} && { flock -w ${wait} 9 || { echo "another boot of this machine held ${BOOT_LOCK_PATH} for ${wait} s" >&2; exit 1; }; } && ${script}`;
655
- }
656
720
  function sleep(ms) {
657
721
  return new Promise((resolve) => setTimeout(resolve, ms));
658
722
  }
@@ -662,12 +726,6 @@ function scrubbed(text, secrets) {
662
726
  function scrubbedTail(text, secrets) {
663
727
  return scrubbed(text, secrets).trim().slice(-STDERR_TAIL_CHARS);
664
728
  }
665
- function imageTreesDirectory(image) {
666
- return `image-${image.replace(/[^A-Za-z0-9._-]/g, "-")}`;
667
- }
668
- function containerHostname(workspaceId) {
669
- return workspaceId.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "").slice(0, 63) || "claxedo";
670
- }
671
729
  var BoatDriverError = class extends Error {
672
730
  constructor(message) {
673
731
  super(message);
@@ -748,43 +806,6 @@ function createBoatSandboxDriver(options) {
748
806
  wait = Math.min(wait * 2, statePollMs.maxMs);
749
807
  }
750
808
  }
751
- function containerScripts(input, staged) {
752
- const port = runtimePort(input);
753
- const directory = workspaceDirectory(input);
754
- const reference = resolveImage(input);
755
- const image = shell(reference);
756
- const trees = `"$(pwd)/${PERSISTENT_ROOT}/${imageTreesDirectory(reference)}"`;
757
- const labelledWithImage = (object, name) => `[ "$(docker ${object} inspect --format '{{index .Config.Labels "${IMAGE_LABEL}"}}' ${name} 2>/dev/null)" = ${image} ]`;
758
- const bootScript = [
759
- `. ${CONTAINER_ENV_PATH}`,
760
- `mkdir -p ${shell(directory)}`,
761
- `cd ${shell(directory)}`,
762
- `exec ${runtimeCommand}`
763
- ].join(" && ");
764
- const mounts = [["workspace", directory], ...PERSISTENT_HOME_MOUNTS];
765
- const run = `docker run -d --name ${containerName} --hostname ${shell(containerHostname(input.workspaceId))} -p ${port}:${port} -v ${VM_DOCKER_INIT}:${CONTAINER_INIT_PATH}:ro -v "$(pwd)/${RUNTIME_ENV_PATH}:${CONTAINER_ENV_PATH}:ro" --env-file ${trees}/${IMAGE_ENV_FILE} ` + IMAGE_TREES.map((tree) => `-v ${trees}/${tree}:/${tree} `).join("") + mounts.map(([source, target]) => `-v "$(pwd)/${PERSISTENT_ROOT}/${source}":${shell(target)} `).join("") + `${CONTAINER_SECURITY_OPTIONS} --entrypoint ${CONTAINER_INIT_PATH} ${STUB_IMAGE} -- sh -lc ${shell(bootScript)}`;
766
- const seedTrees = `sudo rm -rf ${trees} && sudo mkdir -p ${trees} && { docker rm -f ${SEED_CONTAINER_NAME} >/dev/null 2>&1 || true; } && docker create --name ${SEED_CONTAINER_NAME} ${image} true >/dev/null && for tree in ${IMAGE_TREES.join(" ")}; do docker cp ${SEED_CONTAINER_NAME}:/$tree - | sudo tar -xf - -C ${trees} ${IMAGE_TREE_SEED_EXCLUDES.map((exclude) => `--exclude=${exclude}`).join(" ")} && [ -d ${trees}/$tree ] || exit 1; done && docker rm ${SEED_CONTAINER_NAME} >/dev/null && sudo touch ${trees}/${IMAGE_TREES_COMPLETE_MARKER}`;
767
- const seedStub = `stub=$(mktemp -d) && docker image inspect --format '{{range .Config.Env}}{{println .}}{{end}}' ${image} > "$stub/${IMAGE_ENV_FILE}" && workdir=$(docker image inspect --format '{{or .Config.WorkingDir "/"}}' ${image}) && mkdir "$stub/root" ${ROOT_DIRECTORIES.map((directory2) => `"$stub/root/${directory2}"`).join(" ")} && ${ROOT_LINKS.map((link) => `ln -s usr/${link} "$stub/root/${link}"`).join(" && ")} && chmod 1777 "$stub/root/tmp" && tar --owner=0 --group=0 --numeric-owner -cf "$stub/skeleton.tar" -C "$stub/root" . && docker import --change "WORKDIR $workdir" --change ${shell(`LABEL ${IMAGE_LABEL}=${reference}`)} "$stub/skeleton.tar" ${STUB_IMAGE} >/dev/null && docker save -o "$stub/${STUB_IMAGE_ARCHIVE}" ${STUB_IMAGE} && sudo mv "$stub/${IMAGE_ENV_FILE}" "$stub/${STUB_IMAGE_ARCHIVE}" ${trees}/ && rm -rf "$stub"`;
768
- const removeImage = `if docker image inspect ${image} >/dev/null 2>&1; then leftovers=$(docker ps -aq --filter ancestor=${image}) && { [ -z "$leftovers" ] || docker rm -f $leftovers >/dev/null; } && id=$(docker image inspect --format '{{.Id}}' ${image}) && docker rmi ${image} >/dev/null && sudo rm -f ${BOAT_SAVED_IMAGES}/"\${id#sha256:}".tar; fi`;
769
- const seeded = [IMAGE_TREES_COMPLETE_MARKER, STUB_IMAGE_ARCHIVE, IMAGE_ENV_FILE].map((file) => `[ -f ${trees}/${file} ]`).join(" && ");
770
- const prepare = [
771
- `chmod 600 ${staged.env}`,
772
- `find . -maxdepth 1 \\( -name '${RUNTIME_ENV_PATH}.*' -o -name '${REGISTRY_PASSWORD_PATH}.*' \\) -mmin +${BOOT_TTL_SECONDS / 60} -exec rm -f {} +`,
773
- `timeout ${DOCKER_DAEMON_WAIT_SECONDS} sh -c 'until docker info >/dev/null 2>&1; do sleep 1; done'`,
774
- `[ -x ${VM_DOCKER_INIT} ]`,
775
- ...options.registryAuth && staged.password ? [`{ chmod 600 ${staged.password}; docker login ${shell(options.registryAuth.server)} --username ${shell(options.registryAuth.username)} --password-stdin < ${staged.password}; rc=$?; rm -f ${staged.password}; (exit $rc); }`] : [],
776
- // Trees an earlier start marked complete hold what a person or a harness
777
- // changed since, so a seed missing only its stub or env builds those alone.
778
- `if ! { ${seeded}; }; then { docker image inspect ${image} >/dev/null 2>&1 || docker pull ${image} >/dev/null; } && { [ -f ${trees}/${IMAGE_TREES_COMPLETE_MARKER} ] || { ${seedTrees}; }; } && ${seedStub}; fi`,
779
- `{ ${labelledWithImage("image", STUB_IMAGE)} || docker load -i ${trees}/${STUB_IMAGE_ARCHIVE} >/dev/null; }`,
780
- `sudo find "$(pwd)/${PERSISTENT_ROOT}" -mindepth 1 -maxdepth 1 -name 'image-*' ! -path ${trees} -exec rm -rf {} +`
781
- ];
782
- const start = `mv -f ${staged.env} ${RUNTIME_ENV_PATH} && up=; for attempt in $(seq ${CONTAINER_CREATE_ATTEMPTS}); do [ "$attempt" = 1 ] || sleep 1; if ${labelledWithImage("container", containerName)}; then failure=$(docker start ${containerName} 2>&1 >/dev/null) && { up=1; break; }; else docker rm -f ${containerName} >/dev/null 2>&1; failure=$(${run} 2>&1 >/dev/null) && { up=1; break; }; fi; case "$failure" in *${shell(DOCKER_NAME_IN_USE)}*) ;; *) break ;; esac; done; [ -n "$up" ] || { echo "$failure" >&2; false; } && ${removeImage}`;
783
- return {
784
- prepare: holdingBootLock(IMAGE_PREPARATION_TIMEOUT_SECONDS, prepare.join(" && ")),
785
- start: holdingBootLock(CONTAINER_START_TIMEOUT_SECONDS, start)
786
- };
787
- }
788
809
  function healthProbe(port, windowSeconds) {
789
810
  const booting = [...BOOTING_CONTAINER_STATES].join("|");
790
811
  return `end=$(($(date +%s) + ${windowSeconds})); while :; do code=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:${port}/global/health); [ "$code" = 200 ] && break; state=$(docker inspect --format '{{.State.Status}}' ${shell(containerName)} 2>/dev/null || echo missing); case "$state" in ${booting}) ;; *) break ;; esac; [ "$(date +%s)" -lt "$end" ] || break; sleep ${healthIntervalMs / 1e3}; done; echo "$code"; echo "$state"`;
@@ -820,22 +841,41 @@ ${result.stderr}`, secrets), STDERR_TAIL_CHARS);
820
841
  }
821
842
  throw new SandboxRuntimeBootError(`Boat ${sandboxId} runtime did not become healthy: ${last}; ${diagnostic}`);
822
843
  }
823
- async function startContainer(sandboxId, input, hostId) {
844
+ async function startContainer(sandboxId, input, hostId, keepPreviousImage) {
824
845
  const env = { ...bootEnv(input, hostId), ...await options.env?.(input, { id: hostId }) };
825
846
  const boot2 = crypto.randomUUID();
826
847
  const staged = {
827
848
  env: `${RUNTIME_ENV_PATH}.${boot2}`,
828
849
  ...options.registryAuth ? { password: `${REGISTRY_PASSWORD_PATH}.${boot2}` } : {}
829
850
  };
830
- const scripts = containerScripts(input, staged);
851
+ const image = resolveImage(input);
852
+ const scripts = machineBootScripts({
853
+ image,
854
+ keepPreviousImage: keepPreviousImage && image === options.image,
855
+ workspaceId: input.workspaceId,
856
+ directory: workspaceDirectory(input),
857
+ port: runtimePort(input),
858
+ runtimeCommand,
859
+ containerName,
860
+ ...options.registryAuth ? { registry: { server: options.registryAuth.server, username: options.registryAuth.username } } : {},
861
+ staged,
862
+ staleStagedMinutes: BOOT_TTL_SECONDS / 60
863
+ });
831
864
  await client.writeFile(sandboxId, { path: staged.env, content: envFile(env) });
832
865
  if (options.registryAuth && staged.password) {
833
866
  await client.writeFile(sandboxId, { path: staged.password, content: options.registryAuth.password });
834
867
  }
835
868
  const secrets = [...workspaceRuntimeBootSecrets(env), ...options.registryAuth ? [options.registryAuth.password] : []];
836
869
  let failure;
870
+ let booted;
837
871
  try {
838
- await execOrThrow(sandboxId, scripts.prepare, "runtime image preparation", { timeoutSeconds: IMAGE_PREPARATION_TIMEOUT_SECONDS, secrets });
872
+ const prepared = await execOrThrow(sandboxId, scripts.prepare, "runtime image preparation", { timeoutSeconds: IMAGE_PREPARATION_TIMEOUT_SECONDS, secrets }).catch((error) => {
873
+ throw error instanceof BoatDriverError && error.message.includes(IMAGE_DOWNLOAD_INTERRUPTED) ? new SandboxImageDownloadError(error.message) : error;
874
+ });
875
+ booted = bootedImage(prepared.stdout);
876
+ if (!booted) throw new BoatDriverError(`Boat ${sandboxId} runtime image preparation named no image it booted`);
877
+ const skipped = keepPreviousImage ? prepared.stderr.split("\n").find((line) => line.startsWith(KEPT_IMAGE_SKIPPED)) : void 0;
878
+ if (skipped) console.warn("A Boat wake took the deployment's image instead of the one the machine last booted", { sandboxId, workspaceId: input.workspaceId, found: skipped.slice(KEPT_IMAGE_SKIPPED.length).trim() });
839
879
  try {
840
880
  await execOrThrow(sandboxId, scripts.start, "container start", { timeoutSeconds: CONTAINER_START_TIMEOUT_SECONDS, secrets });
841
881
  } catch (error) {
@@ -846,19 +886,19 @@ ${result.stderr}`, secrets), STDERR_TAIL_CHARS);
846
886
  }
847
887
  try {
848
888
  if (options.registryAuth || failure !== void 0) {
849
- await execOrThrow(sandboxId, `rm -f ${[staged.env, staged.password].filter(Boolean).join(" ")}`, "staged file cleanup", { secrets });
889
+ await execOrThrow(sandboxId, `rm -f ${[staged.env, staged.password, bootedImageFile(staged)].filter(Boolean).join(" ")}`, "staged file cleanup", { secrets });
850
890
  }
851
891
  } catch (cleanupFailure) {
852
892
  if (failure === void 0) throw cleanupFailure;
853
893
  if (failure instanceof Error) failure.message = `${failure.message}; ${cleanupFailure instanceof Error ? cleanupFailure.message : String(cleanupFailure)}`;
854
894
  }
855
- if (failure !== void 0) throw failure;
856
- return secrets;
895
+ if (failure !== void 0 || !booted) throw failure;
896
+ return { secrets, booted };
857
897
  }
858
- async function boot(sandboxId, input, hostId) {
898
+ async function boot(sandboxId, input, hostId, keepPreviousImage) {
859
899
  await input.onMachineReady?.();
860
900
  const port = runtimePort(input);
861
- const secrets = await startContainer(sandboxId, input, hostId);
901
+ const { secrets, booted } = await startContainer(sandboxId, input, hostId, keepPreviousImage);
862
902
  await input.onImageReady?.();
863
903
  await execOrThrow(sandboxId, holdingBootLock(HOST_PUBLISH_TIMEOUT_SECONDS, `host ${port} --public`), "host publish", {
864
904
  timeoutSeconds: HOST_PUBLISH_TIMEOUT_SECONDS,
@@ -878,7 +918,7 @@ ${result.stderr}`, secrets), STDERR_TAIL_CHARS);
878
918
  hostId,
879
919
  driverResourceId: sandboxId,
880
920
  driver: { id: "boat", resourceId: sandboxId },
881
- labels: { ...input.labels, ...resolveImage(input) === options.image ? { [SANDBOX_IMAGE_LABEL]: options.image } : {} }
921
+ labels: { ...input.labels, ...resolveImage(input) === options.image ? { [SANDBOX_IMAGE_LABEL]: booted } : {} }
882
922
  };
883
923
  }
884
924
  function assertNetwork(input) {
@@ -927,7 +967,7 @@ ${result.stderr}`, secrets), STDERR_TAIL_CHARS);
927
967
  if (failures.length > 1) throw new AggregateError(failures, `Boat ${created.id} resource handoff and deletion failed`);
928
968
  const ready = await waitUntilReady(created.id);
929
969
  if (!ready) return { provisioning: true, retryAfterMs: 0 };
930
- return boot(created.id, input, hostId);
970
+ return boot(created.id, input, hostId, false);
931
971
  }
932
972
  async function resumeHost(input) {
933
973
  const sandboxId = input.lease.sandboxId;
@@ -947,7 +987,7 @@ ${result.stderr}`, secrets), STDERR_TAIL_CHARS);
947
987
  }
948
988
  const ready = await waitUntilReady(sandboxId);
949
989
  if (!ready) return { provisioning: true, retryAfterMs: 0 };
950
- return boot(sandboxId, input.ensure, hostId);
990
+ return boot(sandboxId, input.ensure, hostId, !input.ensure.updateImage);
951
991
  }
952
992
  async function stop(target) {
953
993
  await client.command(target.sandboxId, {
@@ -983,7 +1023,6 @@ ${result.stderr}`, secrets), STDERR_TAIL_CHARS);
983
1023
  };
984
1024
  }
985
1025
  export {
986
- boatTemplateResetCommand,
987
1026
  createBoatSandboxDriver,
988
1027
  formatBoatRuntimeTemplate,
989
1028
  parseBoatRuntimeTemplate
@@ -31,7 +31,7 @@ var cloudflareBilling = {
31
31
  };
32
32
 
33
33
  // ../sandbox-contract/dist/index.mjs
34
- var sandboxCheckpointUnusableCauses = ["expired", "runtime_outdated", "runtime_modified", "not_restorable"];
34
+ var sandboxCheckpointUnusableCauses = ["expired", "runtime_modified", "not_restorable"];
35
35
  function isSandboxCheckpointUnusableCause(value) {
36
36
  return sandboxCheckpointUnusableCauses.some((cause) => cause === value);
37
37
  }
@@ -311,7 +311,11 @@ function workspaceRuntimeSourceEnv(input) {
311
311
  }
312
312
 
313
313
  // src/runtime-version.ts
314
- var MINIMUM_RESTORABLE_WORKSPACE_RUNTIME_VERSION = "0.5.2";
314
+ var SERVED_WORKSPACE_RUNTIME_FLOOR = {
315
+ version: "0.10.1",
316
+ image: "ghcr.io/kyashrathore/claxedo-sandbox:workspace-runtime-0-10-1-40706b7356-agent-plugins-v8"
317
+ };
318
+ var MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION = SERVED_WORKSPACE_RUNTIME_FLOOR.version;
315
319
 
316
320
  // src/drivers/cloudflare.ts
317
321
  function registryEntries(input) {
@@ -452,7 +456,7 @@ function createCloudflareSandboxDriver(options) {
452
456
  // what makes this driver's `list()` (and therefore GC) possible at all.
453
457
  // Not container env — labels are control-plane metadata.
454
458
  labels: input.labels,
455
- ...input.bootSource?.kind === "driver-snapshot" ? { restore: { snapshotId: input.bootSource.snapshotId }, minimumRuntimeVersion: MINIMUM_RESTORABLE_WORKSPACE_RUNTIME_VERSION } : {},
459
+ ...input.bootSource?.kind === "driver-snapshot" ? { restore: { snapshotId: input.bootSource.snapshotId }, minimumRuntimeVersion: MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION } : {},
456
460
  ...egress ? { egress } : {}
457
461
  }
458
462
  ).catch((error) => {
@@ -91,6 +91,11 @@ var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
91
91
  function workspaceRuntimeVersion() {
92
92
  return process.env.WORKSPACE_RUNTIME_VERSION || DEFAULT_WORKSPACE_RUNTIME_VERSION;
93
93
  }
94
+ var SERVED_WORKSPACE_RUNTIME_FLOOR = {
95
+ version: "0.10.1",
96
+ image: "ghcr.io/kyashrathore/claxedo-sandbox:workspace-runtime-0-10-1-40706b7356-agent-plugins-v8"
97
+ };
98
+ var MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION = SERVED_WORKSPACE_RUNTIME_FLOOR.version;
94
99
 
95
100
  // src/image-name.ts
96
101
  var SNAPSHOT_SCHEMA_VERSION = 8;
@@ -1,9 +1,21 @@
1
1
  import type { SandboxDriver } from "../contract";
2
2
  import { localBrokeringTestDriverCatalogEntry } from "../driver-catalog";
3
+ /**
4
+ * A published runtime image to run instead of `executable`: Docker on the host
5
+ * network, with the paths the runtime reads mounted where they are, and no
6
+ * sandbox-exec network policy, so it runs on Linux. The version-skew suite
7
+ * runs the control plane against the oldest image it still serves this way.
8
+ */
9
+ export type LocalBrokeringContainer = {
10
+ image: string;
11
+ command: string;
12
+ mounts: readonly string[];
13
+ };
3
14
  export type LocalBrokeringDriverOptions = {
4
15
  root: string;
5
16
  executable: string;
6
17
  args: string[];
18
+ container?: LocalBrokeringContainer;
7
19
  allowedOrigins: readonly string[];
8
20
  directOrigins: readonly string[];
9
21
  upstreams?: Readonly<Record<string, string>>;
@@ -793,6 +793,7 @@ ${lines}` : ""}`);
793
793
  }
794
794
  async function closeHost(host) {
795
795
  const child = host.child;
796
+ if (host.container) execFileSync("docker", ["rm", "-f", host.container], { stdio: "ignore" });
796
797
  if (child && child.exitCode === null) {
797
798
  child.kill("SIGTERM");
798
799
  await new Promise((resolve) => child.once("exit", () => resolve()));
@@ -802,8 +803,14 @@ async function closeHost(host) {
802
803
  await new Promise((resolve) => host.proxy.close(() => resolve()));
803
804
  await fs.rm(host.directory, { recursive: true, force: true });
804
805
  }
806
+ function containerRun(container, name, input) {
807
+ const user = `${process.getuid?.() ?? 0}:${process.getgid?.() ?? 0}`;
808
+ const mounts = [.../* @__PURE__ */ new Set([input.root, ...container.mounts])].flatMap((mount) => ["-v", `${mount}:${mount}`]);
809
+ const env = Object.keys(input.env).filter((key) => key !== "PATH").flatMap((key) => ["-e", key]);
810
+ return { name, args: ["run", "--rm", "--name", name, "--network", "host", "--user", user, ...mounts, ...env, "-w", input.workspace, "--entrypoint", container.command, container.image] };
811
+ }
805
812
  function createLocalBrokeringSandboxDriver(options) {
806
- if (process.platform !== "darwin") throw new Error("local brokering test driver requires macOS sandbox-exec network policy");
813
+ if (process.platform !== "darwin" && !options.container) throw new Error("local brokering test driver requires macOS sandbox-exec network policy, or a runtime container");
807
814
  const authority = createTestAuthority(options.root);
808
815
  const hosts = /* @__PURE__ */ new Map();
809
816
  const allowedOrigins = new Set(options.allowedOrigins.map((origin) => new URL(origin).origin));
@@ -976,11 +983,9 @@ function createLocalBrokeringSandboxDriver(options) {
976
983
  const log = openSync(logFile, "w", 384);
977
984
  let child;
978
985
  try {
979
- child = spawn("/usr/bin/sandbox-exec", ["-p", networkPolicy, options.executable, ...options.args], {
980
- cwd: workspace,
981
- env,
982
- stdio: ["ignore", log, log]
983
- });
986
+ const container = options.container ? containerRun(options.container, `claxedo-local-${target.sandboxId}`, { root: options.root, workspace, env }) : void 0;
987
+ if (container) host.container = container.name;
988
+ child = container ? spawn("docker", container.args, { env: { ...process.env, ...env }, stdio: ["ignore", log, log] }) : spawn("/usr/bin/sandbox-exec", ["-p", networkPolicy, options.executable, ...options.args], { cwd: workspace, env, stdio: ["ignore", log, log] });
984
989
  } finally {
985
990
  closeSync(log);
986
991
  }
@@ -73,6 +73,11 @@ var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
73
73
  function workspaceRuntimeVersion() {
74
74
  return process.env.WORKSPACE_RUNTIME_VERSION || DEFAULT_WORKSPACE_RUNTIME_VERSION;
75
75
  }
76
+ var SERVED_WORKSPACE_RUNTIME_FLOOR = {
77
+ version: "0.10.1",
78
+ image: "ghcr.io/kyashrathore/claxedo-sandbox:workspace-runtime-0-10-1-40706b7356-agent-plugins-v8"
79
+ };
80
+ var MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION = SERVED_WORKSPACE_RUNTIME_FLOOR.version;
76
81
 
77
82
  // src/image-name.ts
78
83
  var SNAPSHOT_SCHEMA_VERSION = 8;
@@ -73,6 +73,11 @@ var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
73
73
  function workspaceRuntimeVersion() {
74
74
  return process.env.WORKSPACE_RUNTIME_VERSION || DEFAULT_WORKSPACE_RUNTIME_VERSION;
75
75
  }
76
+ var SERVED_WORKSPACE_RUNTIME_FLOOR = {
77
+ version: "0.10.1",
78
+ image: "ghcr.io/kyashrathore/claxedo-sandbox:workspace-runtime-0-10-1-40706b7356-agent-plugins-v8"
79
+ };
80
+ var MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION = SERVED_WORKSPACE_RUNTIME_FLOOR.version;
76
81
 
77
82
  // src/command.ts
78
83
  function shell(input) {
@@ -3,6 +3,11 @@ var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
3
3
  function workspaceRuntimeVersion() {
4
4
  return process.env.WORKSPACE_RUNTIME_VERSION || DEFAULT_WORKSPACE_RUNTIME_VERSION;
5
5
  }
6
+ var SERVED_WORKSPACE_RUNTIME_FLOOR = {
7
+ version: "0.10.1",
8
+ image: "ghcr.io/kyashrathore/claxedo-sandbox:workspace-runtime-0-10-1-40706b7356-agent-plugins-v8"
9
+ };
10
+ var MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION = SERVED_WORKSPACE_RUNTIME_FLOOR.version;
6
11
 
7
12
  // src/image-name.ts
8
13
  var SNAPSHOT_SCHEMA_VERSION = 8;
package/dist/image.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { RUNTIME_DIR, WORKSPACE_RUNTIME_PORT, WORKSPACE_DIR } from "./defaults";
2
2
  export { SNAPSHOT_SCHEMA_VERSION, snapshotVersion, assertSandboxImageReference, defaultSandboxImage, } from "./image-name";
3
+ export { SERVED_WORKSPACE_RUNTIME_FLOOR } from "./runtime-version";
3
4
  export declare const SANDBOX_IMAGE_REPOSITORY: string;
4
5
  export declare const SANDBOX_IMAGE: string;
package/dist/image.mjs CHANGED
@@ -11,6 +11,11 @@ var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
11
11
  function workspaceRuntimeVersion() {
12
12
  return process.env.WORKSPACE_RUNTIME_VERSION || DEFAULT_WORKSPACE_RUNTIME_VERSION;
13
13
  }
14
+ var SERVED_WORKSPACE_RUNTIME_FLOOR = {
15
+ version: "0.10.1",
16
+ image: "ghcr.io/kyashrathore/claxedo-sandbox:workspace-runtime-0-10-1-40706b7356-agent-plugins-v8"
17
+ };
18
+ var MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION = SERVED_WORKSPACE_RUNTIME_FLOOR.version;
14
19
 
15
20
  // src/image-name.ts
16
21
  var SNAPSHOT_SCHEMA_VERSION = 8;
@@ -45,6 +50,7 @@ export {
45
50
  RUNTIME_DIR,
46
51
  SANDBOX_IMAGE,
47
52
  SANDBOX_IMAGE_REPOSITORY,
53
+ SERVED_WORKSPACE_RUNTIME_FLOOR,
48
54
  SNAPSHOT_SCHEMA_VERSION,
49
55
  WORKSPACE_DIR,
50
56
  WORKSPACE_RUNTIME_PORT,