@claxedo/sandbox-manager 0.10.1 → 0.10.3
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 +100 -30
- package/dist/checkpoint-manager.mjs +19 -0
- package/dist/contract.d.ts +40 -17
- package/dist/driver-billing.d.ts +23 -0
- package/dist/driver-catalog.mjs +57 -3
- package/dist/driver-ensure-input.d.ts +22 -0
- package/dist/drivers/agent37-client.d.ts +87 -0
- package/dist/drivers/agent37.d.ts +25 -0
- package/dist/drivers/agent37.mjs +888 -0
- package/dist/drivers/boat-client.d.ts +4 -4
- package/dist/drivers/boat-machine.d.ts +80 -0
- package/dist/drivers/boat-preparation.test-support.d.ts +2 -0
- package/dist/drivers/boat-template.mjs +196 -92
- package/dist/drivers/boat.d.ts +0 -17
- package/dist/drivers/boat.mjs +200 -108
- package/dist/drivers/cloudflare.d.ts +5 -9
- package/dist/drivers/cloudflare.mjs +71 -7
- package/dist/drivers/docker.mjs +57 -2
- package/dist/drivers/local-brokering.d.ts +12 -0
- package/dist/drivers/local-brokering.mjs +99 -24
- package/dist/drivers/modal.mjs +57 -2
- package/dist/drivers/transient-error.d.ts +0 -1
- package/dist/drivers/vercel.mjs +61 -3
- package/dist/egress-policy.d.ts +33 -32
- package/dist/egress-warnings.d.ts +4 -6
- package/dist/hosted-network-policy.d.ts +10 -64
- package/dist/hosted-network-policy.mjs +1 -1
- package/dist/image-name.d.ts +0 -1
- package/dist/image-name.mjs +5 -0
- package/dist/image.d.ts +1 -0
- package/dist/image.mjs +6 -0
- package/dist/index.mjs +180 -112
- package/dist/json.d.ts +1 -10
- package/dist/lease-failure.d.ts +20 -1
- package/dist/lease-types.d.ts +1 -10
- package/dist/runtime-env.d.ts +6 -0
- package/dist/runtime-env.mjs +5 -0
- package/dist/runtime-version.d.ts +25 -7
- package/dist/runtime-version.mjs +13 -2
- package/dist/start-admission.d.ts +32 -0
- package/dist/start-telemetry.d.ts +5 -3
- package/package.json +12 -4
package/dist/drivers/boat.mjs
CHANGED
|
@@ -31,6 +31,7 @@ var sandboxDriverCredentialFields = {
|
|
|
31
31
|
{ key: "worker_url", label: "Worker URL" }
|
|
32
32
|
],
|
|
33
33
|
boat: [{ key: "api_key", label: "API Key", secret: true }],
|
|
34
|
+
agent37: [{ key: "api_key", label: "API Key", secret: true }],
|
|
34
35
|
docker: [{ key: "image", label: "Image" }]
|
|
35
36
|
};
|
|
36
37
|
var sandboxDriverLabels = {
|
|
@@ -38,6 +39,7 @@ var sandboxDriverLabels = {
|
|
|
38
39
|
vercel: "Vercel",
|
|
39
40
|
cloudflare: "Cloudflare",
|
|
40
41
|
boat: "Boat",
|
|
42
|
+
agent37: "Agent37",
|
|
41
43
|
docker: "Docker"
|
|
42
44
|
};
|
|
43
45
|
|
|
@@ -49,6 +51,13 @@ var SandboxRuntimeBootError = class extends Error {
|
|
|
49
51
|
this.name = "SandboxRuntimeBootError";
|
|
50
52
|
}
|
|
51
53
|
};
|
|
54
|
+
var SandboxImageDownloadError = class extends Error {
|
|
55
|
+
imageDownloadFailed = true;
|
|
56
|
+
constructor(reason) {
|
|
57
|
+
super(reason);
|
|
58
|
+
this.name = "SandboxImageDownloadError";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
52
61
|
var SandboxHostGoneError = class extends Error {
|
|
53
62
|
hostGone = true;
|
|
54
63
|
constructor(reason) {
|
|
@@ -104,6 +113,10 @@ var WORKSPACE_RUNTIME_PUBLIC_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
|
104
113
|
function workspaceRuntimeBootSecrets(env) {
|
|
105
114
|
return Object.entries(env).flatMap(([key, value]) => WORKSPACE_RUNTIME_PUBLIC_ENV_KEYS.has(key) ? [] : [value]);
|
|
106
115
|
}
|
|
116
|
+
var REDACTED_VALUE_MIN_LENGTH = 8;
|
|
117
|
+
function redactWorkspaceRuntimeBootSecrets(text, secrets) {
|
|
118
|
+
return secrets.filter((secret) => secret.length >= REDACTED_VALUE_MIN_LENGTH).reduce((output, secret) => output.split(secret).join("[redacted]"), text);
|
|
119
|
+
}
|
|
107
120
|
function workspaceRuntimeBootEnv(input) {
|
|
108
121
|
assertWorkspaceRuntimeIdentityEnv(input.env);
|
|
109
122
|
const env = {
|
|
@@ -149,6 +162,17 @@ function envFile(input) {
|
|
|
149
162
|
}).join("\n") + "\n";
|
|
150
163
|
}
|
|
151
164
|
|
|
165
|
+
// src/runtime-version.ts
|
|
166
|
+
var SERVED_WORKSPACE_RUNTIME_FLOOR = {
|
|
167
|
+
version: "0.10.1",
|
|
168
|
+
image: "ghcr.io/kyashrathore/claxedo-sandbox:workspace-runtime-0-10-1-40706b7356-agent-plugins-v8"
|
|
169
|
+
};
|
|
170
|
+
var MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION = SERVED_WORKSPACE_RUNTIME_FLOOR.version;
|
|
171
|
+
var SANDBOX_IMAGE_RUNTIME_VERSION_FILE = "opt/workspace-runtime/workspace-runtime-version";
|
|
172
|
+
function servedRuntimeVersionTest(file) {
|
|
173
|
+
return `version=$(cat ${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} ]`;
|
|
174
|
+
}
|
|
175
|
+
|
|
152
176
|
// src/image-name.ts
|
|
153
177
|
function assertSandboxImageReference(image) {
|
|
154
178
|
if (!image || image !== image.trim() || image.startsWith("-") || /\s/.test(image)) {
|
|
@@ -168,6 +192,27 @@ var boatBilling = {
|
|
|
168
192
|
large: { size: "large", vcpu: 8, memoryGb: 16, usdPerHour: 0.072 }
|
|
169
193
|
}
|
|
170
194
|
};
|
|
195
|
+
var AGENT37_USD_PER_MONTH = { vcpu: 0.8, memoryGb: 0.7, diskGb: 0.09 };
|
|
196
|
+
var AGENT37_MONTH_HOURS = 730;
|
|
197
|
+
var agent37MachineShapes = {
|
|
198
|
+
small: { cpu: 2, memory: 4, disk: 10 },
|
|
199
|
+
default: { cpu: 4, memory: 8, disk: 20 },
|
|
200
|
+
large: { cpu: 4, memory: 16, disk: 40 }
|
|
201
|
+
};
|
|
202
|
+
function agent37Rate(shape) {
|
|
203
|
+
const usdPerMonth = shape.cpu * AGENT37_USD_PER_MONTH.vcpu + shape.memory * AGENT37_USD_PER_MONTH.memoryGb + shape.disk * AGENT37_USD_PER_MONTH.diskGb;
|
|
204
|
+
return { size: `${shape.cpu} vCPU / ${shape.memory} GB`, vcpu: shape.cpu, memoryGb: shape.memory, usdPerHour: usdPerMonth / AGENT37_MONTH_HOURS };
|
|
205
|
+
}
|
|
206
|
+
var agent37Billing = {
|
|
207
|
+
source: { url: "https://www.agent37.com/docs/agents-api/billing", readOn: "2026-11-11" },
|
|
208
|
+
meter: "Agent37 meters an instance per minute at a monthly rate over 730 hours: $0.80 per vCPU, $0.70 per GB of memory and $0.09 per GB of disk while it runs, and the disk alone while it is stopped.",
|
|
209
|
+
rates: {
|
|
210
|
+
small: agent37Rate(agent37MachineShapes.small),
|
|
211
|
+
default: agent37Rate(agent37MachineShapes.default),
|
|
212
|
+
large: agent37Rate(agent37MachineShapes.large)
|
|
213
|
+
},
|
|
214
|
+
excludes: `the disk of a stopped instance, billed at $${AGENT37_USD_PER_MONTH.diskGb} per GB a month until the instance is deleted`
|
|
215
|
+
};
|
|
171
216
|
var CLOUDFLARE_USD_PER_GIB_SECOND_MEMORY = 25e-7;
|
|
172
217
|
var CLOUDFLARE_USD_PER_GB_SECOND_DISK = 7e-8;
|
|
173
218
|
var CLOUDFLARE_USD_PER_VCPU_SECOND_ACTIVE = 2e-5;
|
|
@@ -253,8 +298,8 @@ var sandboxDriverCatalog = {
|
|
|
253
298
|
hostWatchesIdle: true,
|
|
254
299
|
targetAccess: "relay",
|
|
255
300
|
secretBrokering: "native",
|
|
256
|
-
//
|
|
257
|
-
egressControl: "
|
|
301
|
+
// The sandbox Worker starts the container without internet and routes each allowed host through itself; a Worker that cannot confirm that refuses the start.
|
|
302
|
+
egressControl: "hosts",
|
|
258
303
|
// A container snapshot holds the whole root filesystem; the platform keeps it 30 days past its last restore and offers no deletion.
|
|
259
304
|
persistence: {
|
|
260
305
|
resume: "replacement-restore",
|
|
@@ -292,6 +337,33 @@ var sandboxDriverCatalog = {
|
|
|
292
337
|
machineClasses: ["small", "default", "large"]
|
|
293
338
|
}
|
|
294
339
|
},
|
|
340
|
+
agent37: {
|
|
341
|
+
id: "agent37",
|
|
342
|
+
label: sandboxDriverLabels.agent37,
|
|
343
|
+
credentialFields: sandboxDriverCredentialFields.agent37,
|
|
344
|
+
runtimeNetwork: { controlPlane: "direct", relay: "configured" },
|
|
345
|
+
runtimeEnv: "callback",
|
|
346
|
+
preparedImage: true,
|
|
347
|
+
billing: agent37Billing,
|
|
348
|
+
metadata: {
|
|
349
|
+
driverRunsIn: ["worker", "node"],
|
|
350
|
+
hostStopBehavior: "suspends-host",
|
|
351
|
+
hostResumeBehavior: "same-host",
|
|
352
|
+
targetAccess: "relay",
|
|
353
|
+
secretBrokering: "none",
|
|
354
|
+
// An instance's network policy lists domains, reached over HTTPS on 443 alone, and IPv4 ranges; private ranges stay blocked whatever it lists.
|
|
355
|
+
egressControl: "hosts-and-cidrs",
|
|
356
|
+
persistence: {
|
|
357
|
+
resume: "same-sandbox",
|
|
358
|
+
capture: "none",
|
|
359
|
+
clone: false,
|
|
360
|
+
captureSource: "not-applicable",
|
|
361
|
+
retention: "provider-managed",
|
|
362
|
+
restoreMount: "same-resource"
|
|
363
|
+
},
|
|
364
|
+
machineClasses: ["small", "default", "large"]
|
|
365
|
+
}
|
|
366
|
+
},
|
|
295
367
|
docker: {
|
|
296
368
|
id: "docker",
|
|
297
369
|
label: sandboxDriverLabels.docker,
|
|
@@ -364,10 +436,10 @@ var BoatApiError = class extends Error {
|
|
|
364
436
|
}
|
|
365
437
|
/** Boat answers 401 or 403 to the API key itself, never to a sandbox it was asked about. */
|
|
366
438
|
providerKeyRefused;
|
|
367
|
-
/** Create, fork and resume each count as one start (Boat's own refusal
|
|
439
|
+
/** Create, fork and resume each count as one start (Boat's own refusal). */
|
|
368
440
|
providerRateLimited;
|
|
369
441
|
startLimit;
|
|
370
|
-
/** The state Boat's refusal gives the sandbox it refused, e.g. `archived` on a 400 `stop_failed
|
|
442
|
+
/** The state Boat's refusal gives the sandbox it refused, e.g. `archived` on a 400 `stop_failed`. */
|
|
371
443
|
sandboxState;
|
|
372
444
|
};
|
|
373
445
|
function startCount(value) {
|
|
@@ -495,7 +567,7 @@ function createBoatClient(options) {
|
|
|
495
567
|
},
|
|
496
568
|
/**
|
|
497
569
|
* `ttlSeconds` sets the auto-stop deadline to now plus that many seconds; `null` removes it.
|
|
498
|
-
* Boat's reference example answers `sandbox.info`; the API answers `sandbox.updated
|
|
570
|
+
* Boat's reference example answers `sandbox.info`; the API answers `sandbox.updated`.
|
|
499
571
|
*/
|
|
500
572
|
async update(id, input) {
|
|
501
573
|
const value = sandbox(await api(path(id), ["sandbox.updated", "sandbox.info"], { method: "PATCH", body: input }));
|
|
@@ -560,6 +632,97 @@ function isTransientDriverError(err, markers) {
|
|
|
560
632
|
}
|
|
561
633
|
var TRANSIENT_DRIVER_RETRY_MS = 2e3;
|
|
562
634
|
|
|
635
|
+
// src/drivers/boat-machine.ts
|
|
636
|
+
var IMAGE_PREPARATION_TIMEOUT_SECONDS = 600;
|
|
637
|
+
var CONTAINER_START_TIMEOUT_SECONDS = 120;
|
|
638
|
+
var DEFAULT_CONTAINER_NAME = "claxedo-runtime";
|
|
639
|
+
var DOCKER_DAEMON_WAIT_SECONDS = 120;
|
|
640
|
+
var RUNTIME_ENV_PATH = ".claxedo-runtime-env";
|
|
641
|
+
var REGISTRY_PASSWORD_PATH = ".claxedo-registry-password";
|
|
642
|
+
var BOOT_LOCK_PATH = "/run/lock/claxedo-boot.lock";
|
|
643
|
+
var BOOT_LOCK_HEADROOM_SECONDS = 60;
|
|
644
|
+
var CONTAINER_ENV_PATH = "/run/claxedo-runtime.env";
|
|
645
|
+
var PERSISTENT_ROOT = "claxedo-persistent";
|
|
646
|
+
var PERSISTENT_HOME_MOUNTS = [["claxedo", "/root/.claxedo"], ["workspace-runtime", "/root/.workspace-runtime"]];
|
|
647
|
+
var IMAGE_TREES = ["root", "usr", "etc", "var", "opt", "home", "srv"];
|
|
648
|
+
var IMAGE_TREE_SEED_EXCLUDES = ["root/.cache", "root/.npm/_cacache"];
|
|
649
|
+
var IMAGE_TREES_COMPLETE_MARKER = ".complete";
|
|
650
|
+
var SEED_CONTAINER_NAME = "claxedo-seed";
|
|
651
|
+
var STUB_IMAGE = "claxedo-runtime-stub";
|
|
652
|
+
var STUB_IMAGE_ARCHIVE = "stub.tar";
|
|
653
|
+
var IMAGE_LABEL = "claxedo.image";
|
|
654
|
+
var IMAGE_ENV_FILE = "image.env";
|
|
655
|
+
var ROOT_LINKS = ["bin", "lib", "lib64", "sbin"];
|
|
656
|
+
var ROOT_DIRECTORIES = ["boot", "dev", "media", "mnt", "proc", "run", "sys", "tmp", ...IMAGE_TREES];
|
|
657
|
+
var BOAT_SAVED_IMAGES = "/var/lib/ascii-docker-images";
|
|
658
|
+
var DOCKER_NAME_IN_USE = "is already in use by container";
|
|
659
|
+
var CONTAINER_CREATE_ATTEMPTS = 5;
|
|
660
|
+
var CONTAINER_SECURITY_OPTIONS = "--security-opt seccomp=unconfined --security-opt apparmor=unconfined";
|
|
661
|
+
var VM_DOCKER_INIT = "/usr/libexec/docker/docker-init";
|
|
662
|
+
var CONTAINER_INIT_PATH = "/claxedo-init";
|
|
663
|
+
function holdingBootLock(commandTimeoutSeconds, script) {
|
|
664
|
+
const wait = commandTimeoutSeconds - BOOT_LOCK_HEADROOM_SECONDS;
|
|
665
|
+
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}`;
|
|
666
|
+
}
|
|
667
|
+
function containerHostname(workspaceId) {
|
|
668
|
+
return workspaceId.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "").slice(0, 63) || "claxedo";
|
|
669
|
+
}
|
|
670
|
+
function bootedImageFile(staged) {
|
|
671
|
+
return `${staged.env}.image`;
|
|
672
|
+
}
|
|
673
|
+
function bootedImage(prepareStdout) {
|
|
674
|
+
return prepareStdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).pop();
|
|
675
|
+
}
|
|
676
|
+
var stubLabel = `docker image inspect --format '{{index .Config.Labels "${IMAGE_LABEL}"}}' ${STUB_IMAGE} 2>/dev/null`;
|
|
677
|
+
var treesFunctions = [
|
|
678
|
+
`trees_of() { printf '%s/${PERSISTENT_ROOT}/image-%s' "$(pwd)" "$(printf %s "$1" | sed 's/[^A-Za-z0-9._-]/-/g')"; }`,
|
|
679
|
+
`seeded() { [ -f "$1/${IMAGE_TREES_COMPLETE_MARKER}" ] && [ -f "$1/${STUB_IMAGE_ARCHIVE}" ] && [ -f "$1/${IMAGE_ENV_FILE}" ]; }`,
|
|
680
|
+
`served() { ${servedRuntimeVersionTest(`"$1/${SANDBOX_IMAGE_RUNTIME_VERSION_FILE}"`)}; }`
|
|
681
|
+
].join(" && ");
|
|
682
|
+
var IMAGE_DOWNLOAD_INTERRUPTED = "image_download_interrupted";
|
|
683
|
+
var KEPT_IMAGE_SKIPPED = "kept_image_skipped:";
|
|
684
|
+
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; }`;
|
|
685
|
+
function machineBootScripts(boot) {
|
|
686
|
+
const named = shell(boot.image);
|
|
687
|
+
const trees = `"$(trees_of ${named})"`;
|
|
688
|
+
const bootedFile = bootedImageFile(boot.staged);
|
|
689
|
+
const containerOfBooted = `[ "$(docker container inspect --format '{{index .Config.Labels "${IMAGE_LABEL}"}}' ${boot.containerName} 2>/dev/null)" = "$booted" ]`;
|
|
690
|
+
const bootScript = [
|
|
691
|
+
`. ${CONTAINER_ENV_PATH}`,
|
|
692
|
+
`mkdir -p ${shell(boot.directory)}`,
|
|
693
|
+
`cd ${shell(boot.directory)}`,
|
|
694
|
+
`exec ${boot.runtimeCommand}`
|
|
695
|
+
].join(" && ");
|
|
696
|
+
const mounts = [["workspace", boot.directory], ...PERSISTENT_HOME_MOUNTS];
|
|
697
|
+
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)}`;
|
|
698
|
+
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}`;
|
|
699
|
+
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"`;
|
|
700
|
+
const seedNamed = `{ docker image inspect ${named} >/dev/null 2>&1 || pull_resuming ${named}; } && { [ -f ${trees}/${IMAGE_TREES_COMPLETE_MARKER} ] || { ${seedTrees}; }; } && ${seedStub}`;
|
|
701
|
+
const keepPrevious = `previous=$(${stubLabel}) && [ -n "$previous" ] && seeded "$(trees_of "$previous")" && served "$(trees_of "$previous")"`;
|
|
702
|
+
const keptImageSkipped = `echo "${KEPT_IMAGE_SKIPPED} stub=\${previous:-none} seeded=$(seeded "$(trees_of "$previous")" && echo yes || echo no) version=$(cat "$(trees_of "$previous")/${SANDBOX_IMAGE_RUNTIME_VERSION_FILE}" 2>/dev/null || echo none)" >&2`;
|
|
703
|
+
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`;
|
|
704
|
+
const prepare = [
|
|
705
|
+
`chmod 600 ${boot.staged.env}`,
|
|
706
|
+
`find . -maxdepth 1 \\( -name '${RUNTIME_ENV_PATH}.*' -o -name '${REGISTRY_PASSWORD_PATH}.*' \\) -mmin +${boot.staleStagedMinutes} -exec rm -f {} +`,
|
|
707
|
+
`timeout ${DOCKER_DAEMON_WAIT_SECONDS} sh -c 'until docker info >/dev/null 2>&1; do sleep 1; done'`,
|
|
708
|
+
`[ -x ${VM_DOCKER_INIT} ]`,
|
|
709
|
+
...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); }`] : [],
|
|
710
|
+
treesFunctions,
|
|
711
|
+
PULL_RESUMING,
|
|
712
|
+
`booted=${named}`,
|
|
713
|
+
`if ! seeded ${trees}; then ` + (boot.keepPreviousImage ? `if ${keepPrevious}; then booted=$previous; else ${keptImageSkipped}; ${seedNamed}; fi` : seedNamed) + `; fi`,
|
|
714
|
+
`{ [ "$(${stubLabel})" = "$booted" ] || docker load -i "$(trees_of "$booted")/${STUB_IMAGE_ARCHIVE}" >/dev/null; }`,
|
|
715
|
+
`sudo find "$(pwd)/${PERSISTENT_ROOT}" -mindepth 1 -maxdepth 1 -name 'image-*' ! -path "$(trees_of "$booted")" -exec rm -rf {} +`,
|
|
716
|
+
`printf '%s\\n' "$booted" > ${bootedFile}`,
|
|
717
|
+
`printf '%s\\n' "$booted"`
|
|
718
|
+
];
|
|
719
|
+
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}`;
|
|
720
|
+
return {
|
|
721
|
+
prepare: holdingBootLock(IMAGE_PREPARATION_TIMEOUT_SECONDS, prepare.join(" && ")),
|
|
722
|
+
start: holdingBootLock(CONTAINER_START_TIMEOUT_SECONDS, start)
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
|
|
563
726
|
// src/drivers/boat.ts
|
|
564
727
|
function formatBoatRuntimeTemplate(template) {
|
|
565
728
|
return JSON.stringify({ image: template.image, shareCode: template.shareCode });
|
|
@@ -579,7 +742,6 @@ function parseBoatRuntimeTemplate(setting) {
|
|
|
579
742
|
}
|
|
580
743
|
var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
|
|
581
744
|
var DEFAULT_WORKSPACE_DIR = "/workspace";
|
|
582
|
-
var DEFAULT_CONTAINER_NAME = "claxedo-runtime";
|
|
583
745
|
var DEFAULT_PROVISION_TIMEOUT_MS = 12e4;
|
|
584
746
|
var DEFAULT_STATE_POLL_MS = { firstMs: 250, maxMs: 2e3 };
|
|
585
747
|
var DEFAULT_HEALTH_TIMEOUT_MS = 10 * 6e4;
|
|
@@ -587,39 +749,11 @@ var DEFAULT_HEALTH_INTERVAL_MS = 100;
|
|
|
587
749
|
var HEALTH_PROBE_WINDOW_SECONDS = 50;
|
|
588
750
|
var HEALTH_PROBE_MARGIN_SECONDS = 10;
|
|
589
751
|
var BOOTING_CONTAINER_STATES = /* @__PURE__ */ new Set(["running", "paused"]);
|
|
590
|
-
var IMAGE_PREPARATION_TIMEOUT_SECONDS = 600;
|
|
591
|
-
var CONTAINER_START_TIMEOUT_SECONDS = 120;
|
|
592
752
|
var HOST_PUBLISH_TIMEOUT_SECONDS = 120;
|
|
593
753
|
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
754
|
var CONTAINER_STOP_GRACE_SECONDS = 30;
|
|
620
755
|
var HOST_RELAY_NOT_STOPPED = "The sandbox could not stop the localhost relay of this port. Try again.";
|
|
621
756
|
var STDERR_TAIL_CHARS = 600;
|
|
622
|
-
var REDACTED_VALUE_MIN_LENGTH = 8;
|
|
623
757
|
var READY_STATES = /* @__PURE__ */ new Set(["ready", "idle", "running"]);
|
|
624
758
|
var PENDING_STATES = /* @__PURE__ */ new Set(["init", "provisioning", "provisioned", "cloning"]);
|
|
625
759
|
var TRANSIENT_MARKERS = ["transport_failed", "idempotency_in_progress"];
|
|
@@ -627,16 +761,6 @@ var GONE_CODES = /* @__PURE__ */ new Set(["not_found", "snapshot_data_expired"])
|
|
|
627
761
|
var RESUME_UNDER_WAY = "sandbox_resuming";
|
|
628
762
|
var NOT_RUNNING_CODES = /* @__PURE__ */ new Set(["machine_not_running", "sandbox_not_ready"]);
|
|
629
763
|
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
764
|
function hostGoneOrThrow(err) {
|
|
641
765
|
if (err instanceof BoatApiError && GONE_CODES.has(err.code)) throw new SandboxHostGoneError(err.message);
|
|
642
766
|
throw err;
|
|
@@ -649,24 +773,11 @@ function archivedOrThrow(err) {
|
|
|
649
773
|
if (err instanceof BoatApiError && err.code === "stop_failed" && err.sandboxState === "archived") return;
|
|
650
774
|
hostGoneOrThrow(err);
|
|
651
775
|
}
|
|
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
776
|
function sleep(ms) {
|
|
657
777
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
658
778
|
}
|
|
659
|
-
function scrubbed(text, secrets) {
|
|
660
|
-
return secrets.filter((secret) => secret.length >= REDACTED_VALUE_MIN_LENGTH).reduce((output, secret) => output.split(secret).join("[redacted]"), text);
|
|
661
|
-
}
|
|
662
779
|
function scrubbedTail(text, secrets) {
|
|
663
|
-
return
|
|
664
|
-
}
|
|
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";
|
|
780
|
+
return redactWorkspaceRuntimeBootSecrets(text, secrets).trim().slice(-STDERR_TAIL_CHARS);
|
|
670
781
|
}
|
|
671
782
|
var BoatDriverError = class extends Error {
|
|
672
783
|
constructor(message) {
|
|
@@ -748,43 +859,6 @@ function createBoatSandboxDriver(options) {
|
|
|
748
859
|
wait = Math.min(wait * 2, statePollMs.maxMs);
|
|
749
860
|
}
|
|
750
861
|
}
|
|
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
862
|
function healthProbe(port, windowSeconds) {
|
|
789
863
|
const booting = [...BOOTING_CONTAINER_STATES].join("|");
|
|
790
864
|
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"`;
|
|
@@ -813,29 +887,48 @@ function createBoatSandboxDriver(options) {
|
|
|
813
887
|
command: `docker inspect --format 'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}}' ${shell(containerName)}; docker logs --tail 20 ${shell(containerName)} 2>&1`,
|
|
814
888
|
timeoutSeconds: 10
|
|
815
889
|
});
|
|
816
|
-
diagnostic = runtimeBootDiagnostic(
|
|
890
|
+
diagnostic = runtimeBootDiagnostic(redactWorkspaceRuntimeBootSecrets(`${result.stdout}
|
|
817
891
|
${result.stderr}`, secrets), STDERR_TAIL_CHARS);
|
|
818
892
|
} catch {
|
|
819
893
|
diagnostic = "container diagnostics unavailable";
|
|
820
894
|
}
|
|
821
895
|
throw new SandboxRuntimeBootError(`Boat ${sandboxId} runtime did not become healthy: ${last}; ${diagnostic}`);
|
|
822
896
|
}
|
|
823
|
-
async function startContainer(sandboxId, input, hostId) {
|
|
897
|
+
async function startContainer(sandboxId, input, hostId, keepPreviousImage) {
|
|
824
898
|
const env = { ...bootEnv(input, hostId), ...await options.env?.(input, { id: hostId }) };
|
|
825
899
|
const boot2 = crypto.randomUUID();
|
|
826
900
|
const staged = {
|
|
827
901
|
env: `${RUNTIME_ENV_PATH}.${boot2}`,
|
|
828
902
|
...options.registryAuth ? { password: `${REGISTRY_PASSWORD_PATH}.${boot2}` } : {}
|
|
829
903
|
};
|
|
830
|
-
const
|
|
904
|
+
const image = resolveImage(input);
|
|
905
|
+
const scripts = machineBootScripts({
|
|
906
|
+
image,
|
|
907
|
+
keepPreviousImage: keepPreviousImage && image === options.image,
|
|
908
|
+
workspaceId: input.workspaceId,
|
|
909
|
+
directory: workspaceDirectory(input),
|
|
910
|
+
port: runtimePort(input),
|
|
911
|
+
runtimeCommand,
|
|
912
|
+
containerName,
|
|
913
|
+
...options.registryAuth ? { registry: { server: options.registryAuth.server, username: options.registryAuth.username } } : {},
|
|
914
|
+
staged,
|
|
915
|
+
staleStagedMinutes: BOOT_TTL_SECONDS / 60
|
|
916
|
+
});
|
|
831
917
|
await client.writeFile(sandboxId, { path: staged.env, content: envFile(env) });
|
|
832
918
|
if (options.registryAuth && staged.password) {
|
|
833
919
|
await client.writeFile(sandboxId, { path: staged.password, content: options.registryAuth.password });
|
|
834
920
|
}
|
|
835
921
|
const secrets = [...workspaceRuntimeBootSecrets(env), ...options.registryAuth ? [options.registryAuth.password] : []];
|
|
836
922
|
let failure;
|
|
923
|
+
let booted;
|
|
837
924
|
try {
|
|
838
|
-
await execOrThrow(sandboxId, scripts.prepare, "runtime image preparation", { timeoutSeconds: IMAGE_PREPARATION_TIMEOUT_SECONDS, secrets })
|
|
925
|
+
const prepared = await execOrThrow(sandboxId, scripts.prepare, "runtime image preparation", { timeoutSeconds: IMAGE_PREPARATION_TIMEOUT_SECONDS, secrets }).catch((error) => {
|
|
926
|
+
throw error instanceof BoatDriverError && error.message.includes(IMAGE_DOWNLOAD_INTERRUPTED) ? new SandboxImageDownloadError(error.message) : error;
|
|
927
|
+
});
|
|
928
|
+
booted = bootedImage(prepared.stdout);
|
|
929
|
+
if (!booted) throw new BoatDriverError(`Boat ${sandboxId} runtime image preparation named no image it booted`);
|
|
930
|
+
const skipped = keepPreviousImage ? prepared.stderr.split("\n").find((line) => line.startsWith(KEPT_IMAGE_SKIPPED)) : void 0;
|
|
931
|
+
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
932
|
try {
|
|
840
933
|
await execOrThrow(sandboxId, scripts.start, "container start", { timeoutSeconds: CONTAINER_START_TIMEOUT_SECONDS, secrets });
|
|
841
934
|
} catch (error) {
|
|
@@ -846,19 +939,19 @@ ${result.stderr}`, secrets), STDERR_TAIL_CHARS);
|
|
|
846
939
|
}
|
|
847
940
|
try {
|
|
848
941
|
if (options.registryAuth || failure !== void 0) {
|
|
849
|
-
await execOrThrow(sandboxId, `rm -f ${[staged.env, staged.password].filter(Boolean).join(" ")}`, "staged file cleanup", { secrets });
|
|
942
|
+
await execOrThrow(sandboxId, `rm -f ${[staged.env, staged.password, bootedImageFile(staged)].filter(Boolean).join(" ")}`, "staged file cleanup", { secrets });
|
|
850
943
|
}
|
|
851
944
|
} catch (cleanupFailure) {
|
|
852
945
|
if (failure === void 0) throw cleanupFailure;
|
|
853
946
|
if (failure instanceof Error) failure.message = `${failure.message}; ${cleanupFailure instanceof Error ? cleanupFailure.message : String(cleanupFailure)}`;
|
|
854
947
|
}
|
|
855
|
-
if (failure !== void 0) throw failure;
|
|
856
|
-
return secrets;
|
|
948
|
+
if (failure !== void 0 || !booted) throw failure;
|
|
949
|
+
return { secrets, booted };
|
|
857
950
|
}
|
|
858
|
-
async function boot(sandboxId, input, hostId) {
|
|
951
|
+
async function boot(sandboxId, input, hostId, keepPreviousImage) {
|
|
859
952
|
await input.onMachineReady?.();
|
|
860
953
|
const port = runtimePort(input);
|
|
861
|
-
const secrets = await startContainer(sandboxId, input, hostId);
|
|
954
|
+
const { secrets, booted } = await startContainer(sandboxId, input, hostId, keepPreviousImage);
|
|
862
955
|
await input.onImageReady?.();
|
|
863
956
|
await execOrThrow(sandboxId, holdingBootLock(HOST_PUBLISH_TIMEOUT_SECONDS, `host ${port} --public`), "host publish", {
|
|
864
957
|
timeoutSeconds: HOST_PUBLISH_TIMEOUT_SECONDS,
|
|
@@ -878,7 +971,7 @@ ${result.stderr}`, secrets), STDERR_TAIL_CHARS);
|
|
|
878
971
|
hostId,
|
|
879
972
|
driverResourceId: sandboxId,
|
|
880
973
|
driver: { id: "boat", resourceId: sandboxId },
|
|
881
|
-
labels: { ...input.labels, ...resolveImage(input) === options.image ? { [SANDBOX_IMAGE_LABEL]:
|
|
974
|
+
labels: { ...input.labels, ...resolveImage(input) === options.image ? { [SANDBOX_IMAGE_LABEL]: booted } : {} }
|
|
882
975
|
};
|
|
883
976
|
}
|
|
884
977
|
function assertNetwork(input) {
|
|
@@ -927,7 +1020,7 @@ ${result.stderr}`, secrets), STDERR_TAIL_CHARS);
|
|
|
927
1020
|
if (failures.length > 1) throw new AggregateError(failures, `Boat ${created.id} resource handoff and deletion failed`);
|
|
928
1021
|
const ready = await waitUntilReady(created.id);
|
|
929
1022
|
if (!ready) return { provisioning: true, retryAfterMs: 0 };
|
|
930
|
-
return boot(created.id, input, hostId);
|
|
1023
|
+
return boot(created.id, input, hostId, false);
|
|
931
1024
|
}
|
|
932
1025
|
async function resumeHost(input) {
|
|
933
1026
|
const sandboxId = input.lease.sandboxId;
|
|
@@ -947,7 +1040,7 @@ ${result.stderr}`, secrets), STDERR_TAIL_CHARS);
|
|
|
947
1040
|
}
|
|
948
1041
|
const ready = await waitUntilReady(sandboxId);
|
|
949
1042
|
if (!ready) return { provisioning: true, retryAfterMs: 0 };
|
|
950
|
-
return boot(sandboxId, input.ensure, hostId);
|
|
1043
|
+
return boot(sandboxId, input.ensure, hostId, !input.ensure.updateImage);
|
|
951
1044
|
}
|
|
952
1045
|
async function stop(target) {
|
|
953
1046
|
await client.command(target.sandboxId, {
|
|
@@ -983,7 +1076,6 @@ ${result.stderr}`, secrets), STDERR_TAIL_CHARS);
|
|
|
983
1076
|
};
|
|
984
1077
|
}
|
|
985
1078
|
export {
|
|
986
|
-
boatTemplateResetCommand,
|
|
987
1079
|
createBoatSandboxDriver,
|
|
988
1080
|
formatBoatRuntimeTemplate,
|
|
989
1081
|
parseBoatRuntimeTemplate
|
|
@@ -5,19 +5,16 @@ export type CloudflareSandboxDriverOptions = {
|
|
|
5
5
|
workerUrl: string;
|
|
6
6
|
/** Bearer token the Worker validates (its `API_TOKEN` secret). */
|
|
7
7
|
apiToken: string;
|
|
8
|
-
/** Port the workspace-runtime listens on inside the
|
|
8
|
+
/** Port the workspace-runtime listens on inside the container, which the Worker's proxy route forwards to. */
|
|
9
9
|
runtimePort?: number;
|
|
10
|
-
/** Command that launches the workspace-runtime inside the image. */
|
|
11
10
|
runtimeCommand?: string;
|
|
12
|
-
/** Workspace directory inside the sandbox. */
|
|
13
11
|
workspaceDir?: string;
|
|
14
12
|
/**
|
|
15
13
|
* Static control-plane config injected into every sandbox so the runtime can
|
|
16
|
-
* verify relay-proxied requests
|
|
17
|
-
* (
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
* managementJwksUrl → WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL (config apply)
|
|
14
|
+
* verify relay-proxied requests; never a per-workspace secret.
|
|
15
|
+
* relayJwksUrl → WORKSPACE_RUNTIME_RELAY_JWKS_URL (relay host token keys)
|
|
16
|
+
* relayVerifyPem → WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM (fallback to JWKS)
|
|
17
|
+
* managementJwksUrl → WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL (config apply)
|
|
21
18
|
*/
|
|
22
19
|
controlEnv?: WorkspaceRuntimeControlEnv;
|
|
23
20
|
/** Default native harness injected as WORKSPACE_RUNTIME_NATIVE_HARNESS, for example "opencode". */
|
|
@@ -26,7 +23,6 @@ export type CloudflareSandboxDriverOptions = {
|
|
|
26
23
|
env?: (input: SandboxDriverEnsureInput, sandbox: {
|
|
27
24
|
id: string;
|
|
28
25
|
}) => Record<string, string> | Promise<Record<string, string>>;
|
|
29
|
-
/** Injected for tests. */
|
|
30
26
|
fetch?: typeof fetch;
|
|
31
27
|
/** ensure-runtime/HTTP timeout. */
|
|
32
28
|
timeoutMs?: number;
|