@claxedo/sandbox-manager 0.10.1 → 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.
package/README.md CHANGED
@@ -108,7 +108,13 @@ nothing. Without a template, or when the organization's key cannot read it, or
108
108
  for any other image, the first start seeds them. The seed runs once per image, at the start that first names it: it pulls the
109
109
  image, streams the trees out of a created container of it with `docker cp` and
110
110
  unpacks them as root, leaving out `~/.cache` (which Boat's own snapshots leave
111
- out too) and npm's cache. The persistent root is created as root too: Docker
111
+ out too) and npm's cache. Boat's link to ghcr resets mid-pull (2 of 8 staging
112
+ updates on 2026-10-22, `failed to copy: … connection reset by peer`), and
113
+ Docker ends a pull at the first reset, keeping the layers it finished and none
114
+ of the ones in flight. So the pull runs again after a transport failure
115
+ (connection reset, unexpected EOF, a timeout) for as long as each run finishes
116
+ a layer no run before it did; any other failure, and a run that finished
117
+ nothing new, fails the boot with the pull's own output. The persistent root is created as root too: Docker
112
118
  made it as root on every sandbox that booted before the trees existed, and the
113
119
  VM user cannot create inside it. The seed then saves beside the trees what the
114
120
  container needs besides them: the image's env (`image.env`, given to the
@@ -152,9 +158,35 @@ the container leaves `running`/`paused`. A sandbox still archiving from a stop,
152
158
  which Boat offers no wait for, is read again after 250 ms, doubling to 2 s.
153
159
 
154
160
  A wake that pulled (before the stub) took 57–77 s, and failed when ghcr reset
155
- the connection. An image update re-seeds:
156
- the new image is pulled and seeded as above, its stub replaces the old one, and
157
- the old image's trees are removed.
161
+ the connection. A wake whose machine holds only an older image keeps serving
162
+ that image: its trees, stub and env are there, and the runtime in them is no
163
+ older than `MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION`. The wake answers
164
+ with that image's label, so the workspace reads as outdated, instead of waiting
165
+ 60–100 s on the new image's pull and seed (staging, 2026-10-21). The update
166
+ runs only when a start asks for it with `updateImage` (a person's Restart to
167
+ update): the new image is pulled and seeded as above, its stub replaces the
168
+ old one, and the old image's trees are removed. That step is a start of its
169
+ own on the serving lease's epoch, whose phases are timed from the update. An
170
+ update whose step fails leaves the lease serving its image and answers the
171
+ start with the driver's reason, which the manager logs, and a start asking for
172
+ the update of a workspace already serving the deployment's image answers it as
173
+ it is. A cold start, and a wake whose older runtime is
174
+ below the floor, takes the new image at once. On staging (2026-10-22, 0.10.1
175
+ to 0.10.2) the wake that kept the image answered in 22 s and the update's step
176
+ ran 92.7 s.
177
+
178
+ `MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION` (`src/runtime-version.ts`) is the one
179
+ floor for every driver: the oldest runtime this control plane and relay serve,
180
+ whether a Cloudflare restore brings it back or a Boat wake keeps it. Below it a
181
+ workspace updates and keeps its files on every driver: a Boat wake takes the
182
+ new image over the same `/workspace` and homes, and a Cloudflare restore
183
+ carries the repository and HOME into the current image. A runtime
184
+ security fix, or a control-plane or relay contract an older runtime cannot
185
+ speak, raises the floor and the workspace-runtime version together, in the
186
+ commit that makes the change; a test keeps the floor at or below the runtime
187
+ version the tree builds. Each connection names the image it serves
188
+ (`runtimeImage`), and every mint of an outdated one is the
189
+ `sandbox.runtime_outdated_served` event.
158
190
 
159
191
  A start runs as two commands: the preparation (daemon, login, seed, stub)
160
192
  changes nothing a running container serves from, so its failure leaves a
@@ -53,11 +53,19 @@ var PUBLIC_API_ERRORS = {
53
53
  enrollment_paused: { status: 403, message: "enrollment_paused" },
54
54
  enrollment_owner_ineligible: { status: 403, message: "enrollment_owner_ineligible" },
55
55
  enrollment_key_version_mismatch: { status: 403, message: "enrollment_key_version_mismatch" },
56
+ enrollment_denied: { status: 403, message: "enrollment_denied" },
57
+ enrollment_approval_expired: { status: 403, message: "enrollment_approval_expired" },
58
+ enrollment_awaiting_approval: { status: 403, message: "enrollment_awaiting_approval" },
56
59
  invitation_invalid: { status: 403, message: "invitation_invalid" },
57
60
  invitation_expired: { status: 410, message: "invitation_expired" },
58
61
  invitation_revoked: { status: 410, message: "invitation_revoked" },
59
62
  invitation_redeemed: { status: 409, message: "invitation_redeemed" },
60
63
  invitation_host_conflict: { status: 409, message: "invitation_host_conflict" },
64
+ pairing_invalid: { status: 403, message: "pairing_invalid" },
65
+ pairing_expired: { status: 410, message: "pairing_expired" },
66
+ pairing_used: { status: 409, message: "pairing_used" },
67
+ pairing_owner_signed_out: { status: 410, message: "pairing_owner_signed_out" },
68
+ pairing_decided: { status: 409, message: "pairing_decided" },
61
69
  enrollment_generation_superseded: { status: 409, message: "enrollment_generation_superseded" },
62
70
  host_assignment_outside_scope: { status: 400, message: "host_assignment_outside_scope" },
63
71
  host_sealing_key_undeclared: { status: 409, message: "host_sealing_key_undeclared" },
@@ -337,6 +337,13 @@ export type SandboxDriverEnsureInput = {
337
337
  net?: SandboxNetworkPolicy;
338
338
  /** Optional snapshot/image to boot from (user-supplied or pre-built). */
339
339
  snapshot?: string;
340
+ /**
341
+ * The person asked for the deployment's image. Without it, a resume of a
342
+ * machine that holds only an older image may keep serving that one and
343
+ * report it, so the workspace reads as outdated instead of waiting on the
344
+ * new image's preparation.
345
+ */
346
+ updateImage?: true;
340
347
  };
341
348
  export type SandboxTarget = {
342
349
  workspaceId?: string;
@@ -503,6 +510,8 @@ export type SandboxManagerInput = {
503
510
  */
504
511
  net?: SandboxNetworkPolicy;
505
512
  snapshot?: string;
513
+ /** @see SandboxDriverEnsureInput.updateImage */
514
+ updateImage?: true;
506
515
  /**
507
516
  * Called by the one ensure whose acquire created the workspace's first
508
517
  * lease, before its driver is asked for anything; a concurrent start that
@@ -551,6 +560,8 @@ export type SandboxEnsureResult = ({
551
560
  * delivered — the provider edge still holds the previous set.
552
561
  */
553
562
  stale?: true;
563
+ /** @see SandboxTargetResult */
564
+ imageOutdated?: true;
554
565
  }) | {
555
566
  status: "provisioning";
556
567
  retryAfterMs: number;
@@ -572,7 +583,7 @@ export type SandboxTargetResult = ({
572
583
  epoch: number;
573
584
  routingId?: string;
574
585
  homeRegion: SandboxRegion;
575
- /** The runtime booted from an image other than the driver's current one; its next ensure boots the current one. */
586
+ /** The runtime booted from an image other than the driver's current one; an ensure asked to `updateImage` boots the current one. */
576
587
  imageOutdated?: true;
577
588
  /** The start that brought this epoch up, as observed; the run still delivering its settings is timed from it. */
578
589
  start?: SandboxEpochStart;
@@ -0,0 +1,21 @@
1
+ import type { SandboxDriver, SandboxDriverEnsureInput, SandboxLease, SandboxManagerInput, SandboxRegion } from "./contract";
2
+ export declare function leaseHostId(driver: SandboxDriver, workspaceId: string, lease: SandboxLease, managerInput: SandboxManagerInput | undefined): string;
3
+ /**
4
+ * Build the driver-facing ensure input.
5
+ *
6
+ * This is the ONLY place a `SandboxDriverEnsureInput` is constructed, which is
7
+ * what makes the egress guarantee structural rather than a promise kept by each
8
+ * call site: `net` is copied across only when `sandboxEgressDisposition` says
9
+ * the driver can enforce it. A driver declaring `egressControl: "none"` — every
10
+ * driver that throws on a restricted policy, plus the two that silently drop it
11
+ * — cannot be reached by one through any path.
12
+ */
13
+ export declare function ensureHostInput(input: {
14
+ driver: SandboxDriver;
15
+ workspaceId: string;
16
+ lease: SandboxLease;
17
+ homeRegion: SandboxRegion;
18
+ managerInput?: SandboxManagerInput;
19
+ leaseEnv?: Record<string, string>;
20
+ appLabel: string;
21
+ }): SandboxDriverEnsureInput;
@@ -0,0 +1,90 @@
1
+ export declare const IMAGE_PREPARATION_TIMEOUT_SECONDS = 600;
2
+ export declare const CONTAINER_START_TIMEOUT_SECONDS = 120;
3
+ export declare const DEFAULT_CONTAINER_NAME = "claxedo-runtime";
4
+ export declare const RUNTIME_ENV_PATH = ".claxedo-runtime-env";
5
+ export declare const REGISTRY_PASSWORD_PATH = ".claxedo-registry-password";
6
+ /**
7
+ * Turns a booted machine into one any workspace can start from: it keeps what
8
+ * every machine of the image shares (the image's trees, stub archive and env,
9
+ * and Boat's ~/.ascii/playbook.json) and removes what this machine's boot made
10
+ * (the runtime container and Boat's record and image archive of it, the
11
+ * workspace, the runtime's state, the boot's env). Nothing the runtime wrote
12
+ * into the image's trees was found there (Boat, 2026-10-16: the trees held only
13
+ * the image's own entries after two boots); what else differs on the machine
14
+ * is Boat's own, and a machine created from the template gets its own
15
+ * machine-id, SSH host keys, hostname and no hosted port.
16
+ */
17
+ export declare function boatTemplateResetCommand(): string;
18
+ export type StagedFiles = {
19
+ env: string;
20
+ password?: string;
21
+ };
22
+ export declare function holdingBootLock(commandTimeoutSeconds: number, script: string): string;
23
+ /**
24
+ * The image a boot names and the one it may keep serving instead. A resume of
25
+ * a machine whose trees hold only an older image keeps that image when its
26
+ * runtime is no older than the oldest the control plane serves, so a wake
27
+ * after a deploy does not wait 60–100 s on the new image's pull and seed
28
+ * (staging, 2026-10-21); the workspace then reads as outdated, and the
29
+ * person's Restart to update asks for the new image with `keepPreviousImage`
30
+ * off. A cold boot has no previous image to keep.
31
+ */
32
+ export type MachineBoot = {
33
+ image: string;
34
+ keepPreviousImage: boolean;
35
+ workspaceId: string;
36
+ directory: string;
37
+ port: number;
38
+ runtimeCommand: string;
39
+ containerName: string;
40
+ registry?: {
41
+ server: string;
42
+ username: string;
43
+ };
44
+ staged: StagedFiles;
45
+ staleStagedMinutes: number;
46
+ };
47
+ /** Where a boot's preparation leaves the image it booted for its container start to read. */
48
+ export declare function bootedImageFile(staged: StagedFiles): string;
49
+ /** The image a preparation booted: the last line it prints, since `docker login` prints before it. */
50
+ export declare function bootedImage(prepareStdout: string): string | undefined;
51
+ /**
52
+ * The command carries only paths: env arrives through the staged file
53
+ * (mounted read-only, sourced by the entrypoint at every container start so
54
+ * multi-line values like PEMs survive — `docker run --env-file` cannot
55
+ * express them), and the registry password is piped to `docker login` from a
56
+ * file that is removed whatever the outcome; a failed login aborts the chain.
57
+ *
58
+ * Two commands. `prepare` changes nothing a running container serves from:
59
+ * it waits for the daemon (a resume reboots the VM, and Docker comes up as a
60
+ * systemd service after Boat reports the sandbox ready), logs in, and settles
61
+ * which image boots: the one named when its trees, stub and env are there;
62
+ * the one the machine last booted when the boot may keep it and its trees
63
+ * hold a runtime still served; otherwise the one named, pulled and seeded
64
+ * where anything is missing. It loads that image's stub when Docker does not
65
+ * hold it, removes every other image's trees, writes the image for `start`
66
+ * and prints it. A failure there leaves a runtime of the previous image
67
+ * serving. `start` then replaces another image's container with one of the
68
+ * booted image, or starts that image's container that is already there: the
69
+ * one the stop kept, which Boat's restore creates again stopped, or one Boat
70
+ * recreated after a stop that was not this driver's; then it removes the
71
+ * image if a seed left it. A failure there leaves nothing serving.
72
+ */
73
+ /**
74
+ * `docker pull` that goes on after the connection under it breaks. Docker ends
75
+ * a pull at the first reset and keeps none of a layer that was still
76
+ * downloading, but keeps every layer it finished, so pulling again continues
77
+ * from those. Boat's link to ghcr resets mid-pull (2 of 8 staging updates on
78
+ * 22 October, `failed to copy: read tcp …: connection reset by peer`). A pull
79
+ * is run again only after such a transport failure and only while each run
80
+ * finishes a layer none before it did, so it ends; any other failure, and a
81
+ * run that finished nothing new, fails with the pull's own output.
82
+ */
83
+ /** The last line a preparation prints when its pull gave up on a registry link that kept breaking. */
84
+ export declare const IMAGE_DOWNLOAD_INTERRUPTED = "image_download_interrupted";
85
+ /** Leads the line a boot that could keep the machine's last image prints when it takes the named one instead, with what it found. */
86
+ export declare const KEPT_IMAGE_SKIPPED = "kept_image_skipped:";
87
+ export declare function machineBootScripts(boot: MachineBoot): {
88
+ prepare: string;
89
+ start: string;
90
+ };
@@ -0,0 +1,2 @@
1
+ /** What a Boat machine's preparation prints when the image its command names boots: that image, as the real script's last line. */
2
+ export declare function preparedImageOutput(command: string): string;
@@ -81,6 +81,13 @@ var SandboxRuntimeBootError = class extends Error {
81
81
  this.name = "SandboxRuntimeBootError";
82
82
  }
83
83
  };
84
+ var SandboxImageDownloadError = class extends Error {
85
+ imageDownloadFailed = true;
86
+ constructor(reason) {
87
+ super(reason);
88
+ this.name = "SandboxImageDownloadError";
89
+ }
90
+ };
84
91
  var SandboxHostGoneError = class extends Error {
85
92
  hostGone = true;
86
93
  constructor(reason) {
@@ -181,6 +188,13 @@ function envFile(input) {
181
188
  }).join("\n") + "\n";
182
189
  }
183
190
 
191
+ // src/runtime-version.ts
192
+ var SERVED_WORKSPACE_RUNTIME_FLOOR = {
193
+ version: "0.10.1",
194
+ image: "ghcr.io/kyashrathore/claxedo-sandbox:workspace-runtime-0-10-1-40706b7356-agent-plugins-v8"
195
+ };
196
+ var MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION = SERVED_WORKSPACE_RUNTIME_FLOOR.version;
197
+
184
198
  // src/image-name.ts
185
199
  function assertSandboxImageReference(image) {
186
200
  if (!image || image !== image.trim() || image.startsWith("-") || /\s/.test(image)) {
@@ -592,21 +606,10 @@ function isTransientDriverError(err, markers) {
592
606
  }
593
607
  var TRANSIENT_DRIVER_RETRY_MS = 2e3;
594
608
 
595
- // src/drivers/boat.ts
596
- var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
597
- var DEFAULT_WORKSPACE_DIR = "/workspace";
598
- var DEFAULT_CONTAINER_NAME = "claxedo-runtime";
599
- var DEFAULT_PROVISION_TIMEOUT_MS = 12e4;
600
- var DEFAULT_STATE_POLL_MS = { firstMs: 250, maxMs: 2e3 };
601
- var DEFAULT_HEALTH_TIMEOUT_MS = 10 * 6e4;
602
- var DEFAULT_HEALTH_INTERVAL_MS = 100;
603
- var HEALTH_PROBE_WINDOW_SECONDS = 50;
604
- var HEALTH_PROBE_MARGIN_SECONDS = 10;
605
- var BOOTING_CONTAINER_STATES = /* @__PURE__ */ new Set(["running", "paused"]);
609
+ // src/drivers/boat-machine.ts
606
610
  var IMAGE_PREPARATION_TIMEOUT_SECONDS = 600;
607
611
  var CONTAINER_START_TIMEOUT_SECONDS = 120;
608
- var HOST_PUBLISH_TIMEOUT_SECONDS = 120;
609
- var BOOT_TTL_SECONDS = 30 * 60;
612
+ var DEFAULT_CONTAINER_NAME = "claxedo-runtime";
610
613
  var DOCKER_DAEMON_WAIT_SECONDS = 120;
611
614
  var RUNTIME_ENV_PATH = ".claxedo-runtime-env";
612
615
  var REGISTRY_PASSWORD_PATH = ".claxedo-registry-password";
@@ -632,6 +635,92 @@ var CONTAINER_CREATE_ATTEMPTS = 5;
632
635
  var CONTAINER_SECURITY_OPTIONS = "--security-opt seccomp=unconfined --security-opt apparmor=unconfined";
633
636
  var VM_DOCKER_INIT = "/usr/libexec/docker/docker-init";
634
637
  var CONTAINER_INIT_PATH = "/claxedo-init";
638
+ function boatTemplateResetCommand() {
639
+ const bootFiles = [
640
+ `${PERSISTENT_ROOT}/workspace`,
641
+ ...PERSISTENT_HOME_MOUNTS.map(([source]) => `${PERSISTENT_ROOT}/${source}`),
642
+ RUNTIME_ENV_PATH,
643
+ `${RUNTIME_ENV_PATH}.*`,
644
+ `${REGISTRY_PASSWORD_PATH}.*`
645
+ ];
646
+ 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}`;
647
+ }
648
+ function holdingBootLock(commandTimeoutSeconds, script) {
649
+ const wait = commandTimeoutSeconds - BOOT_LOCK_HEADROOM_SECONDS;
650
+ 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}`;
651
+ }
652
+ function containerHostname(workspaceId) {
653
+ return workspaceId.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "").slice(0, 63) || "claxedo";
654
+ }
655
+ function bootedImageFile(staged) {
656
+ return `${staged.env}.image`;
657
+ }
658
+ function bootedImage(prepareStdout) {
659
+ return prepareStdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).pop();
660
+ }
661
+ var VERSION_FILE = "opt/workspace-runtime/workspace-runtime-version";
662
+ var stubLabel = `docker image inspect --format '{{index .Config.Labels "${IMAGE_LABEL}"}}' ${STUB_IMAGE} 2>/dev/null`;
663
+ var treesFunctions = [
664
+ `trees_of() { printf '%s/${PERSISTENT_ROOT}/image-%s' "$(pwd)" "$(printf %s "$1" | sed 's/[^A-Za-z0-9._-]/-/g')"; }`,
665
+ `seeded() { [ -f "$1/${IMAGE_TREES_COMPLETE_MARKER}" ] && [ -f "$1/${STUB_IMAGE_ARCHIVE}" ] && [ -f "$1/${IMAGE_ENV_FILE}" ]; }`,
666
+ `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} ]; }`
667
+ ].join(" && ");
668
+ var IMAGE_DOWNLOAD_INTERRUPTED = "image_download_interrupted";
669
+ var KEPT_IMAGE_SKIPPED = "kept_image_skipped:";
670
+ 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; }`;
671
+ function machineBootScripts(boot) {
672
+ const named = shell(boot.image);
673
+ const trees = `"$(trees_of ${named})"`;
674
+ const bootedFile = bootedImageFile(boot.staged);
675
+ const containerOfBooted = `[ "$(docker container inspect --format '{{index .Config.Labels "${IMAGE_LABEL}"}}' ${boot.containerName} 2>/dev/null)" = "$booted" ]`;
676
+ const bootScript = [
677
+ `. ${CONTAINER_ENV_PATH}`,
678
+ `mkdir -p ${shell(boot.directory)}`,
679
+ `cd ${shell(boot.directory)}`,
680
+ `exec ${boot.runtimeCommand}`
681
+ ].join(" && ");
682
+ const mounts = [["workspace", boot.directory], ...PERSISTENT_HOME_MOUNTS];
683
+ 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)}`;
684
+ 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}`;
685
+ 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"`;
686
+ const seedNamed = `{ docker image inspect ${named} >/dev/null 2>&1 || pull_resuming ${named}; } && { [ -f ${trees}/${IMAGE_TREES_COMPLETE_MARKER} ] || { ${seedTrees}; }; } && ${seedStub}`;
687
+ const keepPrevious = `previous=$(${stubLabel}) && [ -n "$previous" ] && seeded "$(trees_of "$previous")" && served "$(trees_of "$previous")"`;
688
+ 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`;
689
+ 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`;
690
+ const prepare = [
691
+ `chmod 600 ${boot.staged.env}`,
692
+ `find . -maxdepth 1 \\( -name '${RUNTIME_ENV_PATH}.*' -o -name '${REGISTRY_PASSWORD_PATH}.*' \\) -mmin +${boot.staleStagedMinutes} -exec rm -f {} +`,
693
+ `timeout ${DOCKER_DAEMON_WAIT_SECONDS} sh -c 'until docker info >/dev/null 2>&1; do sleep 1; done'`,
694
+ `[ -x ${VM_DOCKER_INIT} ]`,
695
+ ...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); }`] : [],
696
+ treesFunctions,
697
+ PULL_RESUMING,
698
+ `booted=${named}`,
699
+ `if ! seeded ${trees}; then ` + (boot.keepPreviousImage ? `if ${keepPrevious}; then booted=$previous; else ${keptImageSkipped}; ${seedNamed}; fi` : seedNamed) + `; fi`,
700
+ `{ [ "$(${stubLabel})" = "$booted" ] || docker load -i "$(trees_of "$booted")/${STUB_IMAGE_ARCHIVE}" >/dev/null; }`,
701
+ `sudo find "$(pwd)/${PERSISTENT_ROOT}" -mindepth 1 -maxdepth 1 -name 'image-*' ! -path "$(trees_of "$booted")" -exec rm -rf {} +`,
702
+ `printf '%s\\n' "$booted" > ${bootedFile}`,
703
+ `printf '%s\\n' "$booted"`
704
+ ];
705
+ 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}`;
706
+ return {
707
+ prepare: holdingBootLock(IMAGE_PREPARATION_TIMEOUT_SECONDS, prepare.join(" && ")),
708
+ start: holdingBootLock(CONTAINER_START_TIMEOUT_SECONDS, start)
709
+ };
710
+ }
711
+
712
+ // src/drivers/boat.ts
713
+ var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
714
+ var DEFAULT_WORKSPACE_DIR = "/workspace";
715
+ var DEFAULT_PROVISION_TIMEOUT_MS = 12e4;
716
+ var DEFAULT_STATE_POLL_MS = { firstMs: 250, maxMs: 2e3 };
717
+ var DEFAULT_HEALTH_TIMEOUT_MS = 10 * 6e4;
718
+ var DEFAULT_HEALTH_INTERVAL_MS = 100;
719
+ var HEALTH_PROBE_WINDOW_SECONDS = 50;
720
+ var HEALTH_PROBE_MARGIN_SECONDS = 10;
721
+ var BOOTING_CONTAINER_STATES = /* @__PURE__ */ new Set(["running", "paused"]);
722
+ var HOST_PUBLISH_TIMEOUT_SECONDS = 120;
723
+ var BOOT_TTL_SECONDS = 30 * 60;
635
724
  var CONTAINER_STOP_GRACE_SECONDS = 30;
636
725
  var HOST_RELAY_NOT_STOPPED = "The sandbox could not stop the localhost relay of this port. Try again.";
637
726
  var STDERR_TAIL_CHARS = 600;
@@ -643,16 +732,6 @@ var GONE_CODES = /* @__PURE__ */ new Set(["not_found", "snapshot_data_expired"])
643
732
  var RESUME_UNDER_WAY = "sandbox_resuming";
644
733
  var NOT_RUNNING_CODES = /* @__PURE__ */ new Set(["machine_not_running", "sandbox_not_ready"]);
645
734
  var TEMPLATE_UNREADABLE_CODES = /* @__PURE__ */ new Set(["snapshot_not_found", "api_key_action_forbidden", "api_key_sandbox_forbidden"]);
646
- function boatTemplateResetCommand() {
647
- const bootFiles = [
648
- `${PERSISTENT_ROOT}/workspace`,
649
- ...PERSISTENT_HOME_MOUNTS.map(([source]) => `${PERSISTENT_ROOT}/${source}`),
650
- RUNTIME_ENV_PATH,
651
- `${RUNTIME_ENV_PATH}.*`,
652
- `${REGISTRY_PASSWORD_PATH}.*`
653
- ];
654
- 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}`;
655
- }
656
735
  function hostGoneOrThrow(err) {
657
736
  if (err instanceof BoatApiError && GONE_CODES.has(err.code)) throw new SandboxHostGoneError(err.message);
658
737
  throw err;
@@ -665,10 +744,6 @@ function archivedOrThrow(err) {
665
744
  if (err instanceof BoatApiError && err.code === "stop_failed" && err.sandboxState === "archived") return;
666
745
  hostGoneOrThrow(err);
667
746
  }
668
- function holdingBootLock(commandTimeoutSeconds, script) {
669
- const wait = commandTimeoutSeconds - BOOT_LOCK_HEADROOM_SECONDS;
670
- 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}`;
671
- }
672
747
  function sleep(ms) {
673
748
  return new Promise((resolve) => setTimeout(resolve, ms));
674
749
  }
@@ -678,12 +753,6 @@ function scrubbed(text, secrets) {
678
753
  function scrubbedTail(text, secrets) {
679
754
  return scrubbed(text, secrets).trim().slice(-STDERR_TAIL_CHARS);
680
755
  }
681
- function imageTreesDirectory(image) {
682
- return `image-${image.replace(/[^A-Za-z0-9._-]/g, "-")}`;
683
- }
684
- function containerHostname(workspaceId) {
685
- return workspaceId.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "").slice(0, 63) || "claxedo";
686
- }
687
756
  var BoatDriverError = class extends Error {
688
757
  constructor(message) {
689
758
  super(message);
@@ -764,43 +833,6 @@ function createBoatSandboxDriver(options) {
764
833
  wait = Math.min(wait * 2, statePollMs.maxMs);
765
834
  }
766
835
  }
767
- function containerScripts(input, staged) {
768
- const port = runtimePort(input);
769
- const directory = workspaceDirectory(input);
770
- const reference = resolveImage(input);
771
- const image = shell(reference);
772
- const trees = `"$(pwd)/${PERSISTENT_ROOT}/${imageTreesDirectory(reference)}"`;
773
- const labelledWithImage = (object, name) => `[ "$(docker ${object} inspect --format '{{index .Config.Labels "${IMAGE_LABEL}"}}' ${name} 2>/dev/null)" = ${image} ]`;
774
- const bootScript = [
775
- `. ${CONTAINER_ENV_PATH}`,
776
- `mkdir -p ${shell(directory)}`,
777
- `cd ${shell(directory)}`,
778
- `exec ${runtimeCommand}`
779
- ].join(" && ");
780
- const mounts = [["workspace", directory], ...PERSISTENT_HOME_MOUNTS];
781
- 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)}`;
782
- 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}`;
783
- 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"`;
784
- 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`;
785
- const seeded = [IMAGE_TREES_COMPLETE_MARKER, STUB_IMAGE_ARCHIVE, IMAGE_ENV_FILE].map((file) => `[ -f ${trees}/${file} ]`).join(" && ");
786
- const prepare = [
787
- `chmod 600 ${staged.env}`,
788
- `find . -maxdepth 1 \\( -name '${RUNTIME_ENV_PATH}.*' -o -name '${REGISTRY_PASSWORD_PATH}.*' \\) -mmin +${BOOT_TTL_SECONDS / 60} -exec rm -f {} +`,
789
- `timeout ${DOCKER_DAEMON_WAIT_SECONDS} sh -c 'until docker info >/dev/null 2>&1; do sleep 1; done'`,
790
- `[ -x ${VM_DOCKER_INIT} ]`,
791
- ...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); }`] : [],
792
- // Trees an earlier start marked complete hold what a person or a harness
793
- // changed since, so a seed missing only its stub or env builds those alone.
794
- `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`,
795
- `{ ${labelledWithImage("image", STUB_IMAGE)} || docker load -i ${trees}/${STUB_IMAGE_ARCHIVE} >/dev/null; }`,
796
- `sudo find "$(pwd)/${PERSISTENT_ROOT}" -mindepth 1 -maxdepth 1 -name 'image-*' ! -path ${trees} -exec rm -rf {} +`
797
- ];
798
- 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}`;
799
- return {
800
- prepare: holdingBootLock(IMAGE_PREPARATION_TIMEOUT_SECONDS, prepare.join(" && ")),
801
- start: holdingBootLock(CONTAINER_START_TIMEOUT_SECONDS, start)
802
- };
803
- }
804
836
  function healthProbe(port, windowSeconds) {
805
837
  const booting = [...BOOTING_CONTAINER_STATES].join("|");
806
838
  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"`;
@@ -836,22 +868,41 @@ ${result.stderr}`, secrets), STDERR_TAIL_CHARS);
836
868
  }
837
869
  throw new SandboxRuntimeBootError(`Boat ${sandboxId} runtime did not become healthy: ${last}; ${diagnostic}`);
838
870
  }
839
- async function startContainer(sandboxId, input, hostId) {
871
+ async function startContainer(sandboxId, input, hostId, keepPreviousImage) {
840
872
  const env = { ...bootEnv(input, hostId), ...await options.env?.(input, { id: hostId }) };
841
873
  const boot2 = crypto.randomUUID();
842
874
  const staged = {
843
875
  env: `${RUNTIME_ENV_PATH}.${boot2}`,
844
876
  ...options.registryAuth ? { password: `${REGISTRY_PASSWORD_PATH}.${boot2}` } : {}
845
877
  };
846
- const scripts = containerScripts(input, staged);
878
+ const image = resolveImage(input);
879
+ const scripts = machineBootScripts({
880
+ image,
881
+ keepPreviousImage: keepPreviousImage && image === options.image,
882
+ workspaceId: input.workspaceId,
883
+ directory: workspaceDirectory(input),
884
+ port: runtimePort(input),
885
+ runtimeCommand,
886
+ containerName,
887
+ ...options.registryAuth ? { registry: { server: options.registryAuth.server, username: options.registryAuth.username } } : {},
888
+ staged,
889
+ staleStagedMinutes: BOOT_TTL_SECONDS / 60
890
+ });
847
891
  await client.writeFile(sandboxId, { path: staged.env, content: envFile(env) });
848
892
  if (options.registryAuth && staged.password) {
849
893
  await client.writeFile(sandboxId, { path: staged.password, content: options.registryAuth.password });
850
894
  }
851
895
  const secrets = [...workspaceRuntimeBootSecrets(env), ...options.registryAuth ? [options.registryAuth.password] : []];
852
896
  let failure;
897
+ let booted;
853
898
  try {
854
- await execOrThrow(sandboxId, scripts.prepare, "runtime image preparation", { timeoutSeconds: IMAGE_PREPARATION_TIMEOUT_SECONDS, secrets });
899
+ const prepared = await execOrThrow(sandboxId, scripts.prepare, "runtime image preparation", { timeoutSeconds: IMAGE_PREPARATION_TIMEOUT_SECONDS, secrets }).catch((error) => {
900
+ throw error instanceof BoatDriverError && error.message.includes(IMAGE_DOWNLOAD_INTERRUPTED) ? new SandboxImageDownloadError(error.message) : error;
901
+ });
902
+ booted = bootedImage(prepared.stdout);
903
+ if (!booted) throw new BoatDriverError(`Boat ${sandboxId} runtime image preparation named no image it booted`);
904
+ const skipped = keepPreviousImage ? prepared.stderr.split("\n").find((line) => line.startsWith(KEPT_IMAGE_SKIPPED)) : void 0;
905
+ 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() });
855
906
  try {
856
907
  await execOrThrow(sandboxId, scripts.start, "container start", { timeoutSeconds: CONTAINER_START_TIMEOUT_SECONDS, secrets });
857
908
  } catch (error) {
@@ -862,19 +913,19 @@ ${result.stderr}`, secrets), STDERR_TAIL_CHARS);
862
913
  }
863
914
  try {
864
915
  if (options.registryAuth || failure !== void 0) {
865
- await execOrThrow(sandboxId, `rm -f ${[staged.env, staged.password].filter(Boolean).join(" ")}`, "staged file cleanup", { secrets });
916
+ await execOrThrow(sandboxId, `rm -f ${[staged.env, staged.password, bootedImageFile(staged)].filter(Boolean).join(" ")}`, "staged file cleanup", { secrets });
866
917
  }
867
918
  } catch (cleanupFailure) {
868
919
  if (failure === void 0) throw cleanupFailure;
869
920
  if (failure instanceof Error) failure.message = `${failure.message}; ${cleanupFailure instanceof Error ? cleanupFailure.message : String(cleanupFailure)}`;
870
921
  }
871
- if (failure !== void 0) throw failure;
872
- return secrets;
922
+ if (failure !== void 0 || !booted) throw failure;
923
+ return { secrets, booted };
873
924
  }
874
- async function boot(sandboxId, input, hostId) {
925
+ async function boot(sandboxId, input, hostId, keepPreviousImage) {
875
926
  await input.onMachineReady?.();
876
927
  const port = runtimePort(input);
877
- const secrets = await startContainer(sandboxId, input, hostId);
928
+ const { secrets, booted } = await startContainer(sandboxId, input, hostId, keepPreviousImage);
878
929
  await input.onImageReady?.();
879
930
  await execOrThrow(sandboxId, holdingBootLock(HOST_PUBLISH_TIMEOUT_SECONDS, `host ${port} --public`), "host publish", {
880
931
  timeoutSeconds: HOST_PUBLISH_TIMEOUT_SECONDS,
@@ -894,7 +945,7 @@ ${result.stderr}`, secrets), STDERR_TAIL_CHARS);
894
945
  hostId,
895
946
  driverResourceId: sandboxId,
896
947
  driver: { id: "boat", resourceId: sandboxId },
897
- labels: { ...input.labels, ...resolveImage(input) === options.image ? { [SANDBOX_IMAGE_LABEL]: options.image } : {} }
948
+ labels: { ...input.labels, ...resolveImage(input) === options.image ? { [SANDBOX_IMAGE_LABEL]: booted } : {} }
898
949
  };
899
950
  }
900
951
  function assertNetwork(input) {
@@ -943,7 +994,7 @@ ${result.stderr}`, secrets), STDERR_TAIL_CHARS);
943
994
  if (failures.length > 1) throw new AggregateError(failures, `Boat ${created.id} resource handoff and deletion failed`);
944
995
  const ready = await waitUntilReady(created.id);
945
996
  if (!ready) return { provisioning: true, retryAfterMs: 0 };
946
- return boot(created.id, input, hostId);
997
+ return boot(created.id, input, hostId, false);
947
998
  }
948
999
  async function resumeHost(input) {
949
1000
  const sandboxId = input.lease.sandboxId;
@@ -963,7 +1014,7 @@ ${result.stderr}`, secrets), STDERR_TAIL_CHARS);
963
1014
  }
964
1015
  const ready = await waitUntilReady(sandboxId);
965
1016
  if (!ready) return { provisioning: true, retryAfterMs: 0 };
966
- return boot(sandboxId, input.ensure, hostId);
1017
+ return boot(sandboxId, input.ensure, hostId, !input.ensure.updateImage);
967
1018
  }
968
1019
  async function stop(target) {
969
1020
  await client.command(target.sandboxId, {
@@ -49,16 +49,4 @@ export type BoatSandboxDriverOptions = {
49
49
  operationTimeoutMs?: number;
50
50
  fetchImpl?: BoatFetch;
51
51
  };
52
- /**
53
- * Turns a booted machine into one any workspace can start from: it keeps what
54
- * every machine of the image shares (the image's trees, stub archive and env,
55
- * and Boat's ~/.ascii/playbook.json) and removes what this machine's boot made
56
- * (the runtime container and Boat's record and image archive of it, the
57
- * workspace, the runtime's state, the boot's env). Nothing the runtime wrote
58
- * into the image's trees was found there (Boat, 2026-10-16: the trees held only
59
- * the image's own entries after two boots); what else differs on the machine
60
- * is Boat's own, and a machine created from the template gets its own
61
- * machine-id, SSH host keys, hostname and no hosted port.
62
- */
63
- export declare function boatTemplateResetCommand(): string;
64
52
  export declare function createBoatSandboxDriver(options: BoatSandboxDriverOptions): SandboxDriver;