@claxedo/sandbox-manager 0.10.2 → 0.10.4
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 +67 -29
- package/dist/checkpoint-manager.mjs +11 -0
- package/dist/contract.d.ts +28 -16
- package/dist/driver-billing.d.ts +23 -0
- package/dist/driver-catalog.d.ts +1 -2
- package/dist/driver-catalog.mjs +61 -11
- package/dist/driver-ensure-input.d.ts +3 -2
- package/dist/drivers/agent37-client.d.ts +87 -0
- package/dist/drivers/agent37.d.ts +25 -0
- package/dist/drivers/agent37.mjs +886 -0
- package/dist/drivers/boat-client.d.ts +5 -5
- package/dist/drivers/boat-machine.d.ts +7 -17
- package/dist/drivers/boat-template.mjs +87 -35
- package/dist/drivers/boat.d.ts +0 -5
- package/dist/drivers/boat.mjs +81 -26
- package/dist/drivers/cloudflare.d.ts +5 -9
- package/dist/drivers/cloudflare.mjs +97 -43
- package/dist/drivers/docker.mjs +70 -19
- package/dist/drivers/local-brokering.mjs +90 -18
- package/dist/drivers/modal.mjs +56 -11
- package/dist/drivers/transient-error.d.ts +0 -1
- package/dist/drivers/vercel.mjs +65 -15
- 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/index.mjs +103 -78
- package/dist/json.d.ts +1 -13
- package/dist/lease-failure.d.ts +3 -3
- package/dist/lease-types.d.ts +1 -10
- package/dist/runtime-env.d.ts +6 -7
- package/dist/runtime-env.mjs +5 -10
- package/dist/runtime-version.d.ts +4 -0
- package/dist/runtime-version.mjs +6 -0
- package/dist/start-admission.d.ts +32 -0
- package/package.json +12 -4
|
@@ -18,7 +18,7 @@ export type BoatFetch = (input: string, init?: RequestInit) => Promise<Response>
|
|
|
18
18
|
/**
|
|
19
19
|
* What Boat's 429 `rate_limited` says about the account's start limit: the
|
|
20
20
|
* window that is used up, its limit, and the starts used of each window. It
|
|
21
|
-
* names no time the window reopens
|
|
21
|
+
* names no time the window reopens, so none is kept.
|
|
22
22
|
*/
|
|
23
23
|
export type BoatStartLimit = {
|
|
24
24
|
window: SandboxStartWindow;
|
|
@@ -33,10 +33,10 @@ export declare class BoatApiError extends Error {
|
|
|
33
33
|
readonly status?: number | undefined;
|
|
34
34
|
/** Boat answers 401 or 403 to the API key itself, never to a sandbox it was asked about. */
|
|
35
35
|
readonly providerKeyRefused: boolean;
|
|
36
|
-
/** Create, fork and resume each count as one start (Boat's own refusal
|
|
36
|
+
/** Create, fork and resume each count as one start (Boat's own refusal). */
|
|
37
37
|
readonly providerRateLimited?: SandboxStartLimit;
|
|
38
38
|
readonly startLimit?: BoatStartLimit;
|
|
39
|
-
/** The state Boat's refusal gives the sandbox it refused, e.g. `archived` on a 400 `stop_failed
|
|
39
|
+
/** The state Boat's refusal gives the sandbox it refused, e.g. `archived` on a 400 `stop_failed`. */
|
|
40
40
|
readonly sandboxState?: BoatSandboxState;
|
|
41
41
|
constructor(message: string, code: string, status?: number | undefined, refusal?: {
|
|
42
42
|
startLimit?: BoatStartLimit;
|
|
@@ -44,7 +44,7 @@ export declare class BoatApiError extends Error {
|
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
46
|
export declare function invalid(): never;
|
|
47
|
-
export declare function
|
|
47
|
+
export declare function responseString(value: unknown): string;
|
|
48
48
|
export type BoatClientOptions = {
|
|
49
49
|
apiKey: string;
|
|
50
50
|
baseUrl?: string;
|
|
@@ -75,7 +75,7 @@ export declare function createBoatClient(options: BoatClientOptions): {
|
|
|
75
75
|
}): Promise<void>;
|
|
76
76
|
/**
|
|
77
77
|
* `ttlSeconds` sets the auto-stop deadline to now plus that many seconds; `null` removes it.
|
|
78
|
-
* Boat's reference example answers `sandbox.info`; the API answers `sandbox.updated
|
|
78
|
+
* Boat's reference example answers `sandbox.info`; the API answers `sandbox.updated`.
|
|
79
79
|
*/
|
|
80
80
|
update(id: string, input: {
|
|
81
81
|
ttlSeconds: number | null;
|
|
@@ -9,8 +9,8 @@ export declare const REGISTRY_PASSWORD_PATH = ".claxedo-registry-password";
|
|
|
9
9
|
* and Boat's ~/.ascii/playbook.json) and removes what this machine's boot made
|
|
10
10
|
* (the runtime container and Boat's record and image archive of it, the
|
|
11
11
|
* workspace, the runtime's state, the boot's env). Nothing the runtime wrote
|
|
12
|
-
* into the image's trees was found there (
|
|
13
|
-
*
|
|
12
|
+
* into the image's trees was found there (the trees held only the image's own
|
|
13
|
+
* entries after two boots); what else differs on the machine
|
|
14
14
|
* is Boat's own, and a machine created from the template gets its own
|
|
15
15
|
* machine-id, SSH host keys, hostname and no hosted port.
|
|
16
16
|
*/
|
|
@@ -25,7 +25,7 @@ export declare function holdingBootLock(commandTimeoutSeconds: number, script: s
|
|
|
25
25
|
* a machine whose trees hold only an older image keeps that image when its
|
|
26
26
|
* runtime is no older than the oldest the control plane serves, so a wake
|
|
27
27
|
* after a deploy does not wait 60–100 s on the new image's pull and seed
|
|
28
|
-
* (staging
|
|
28
|
+
* (staging); the workspace then reads as outdated, and the
|
|
29
29
|
* person's Restart to update asks for the new image with `keepPreviousImage`
|
|
30
30
|
* off. A cold boot has no previous image to keep.
|
|
31
31
|
*/
|
|
@@ -48,6 +48,10 @@ export type MachineBoot = {
|
|
|
48
48
|
export declare function bootedImageFile(staged: StagedFiles): string;
|
|
49
49
|
/** The image a preparation booted: the last line it prints, since `docker login` prints before it. */
|
|
50
50
|
export declare function bootedImage(prepareStdout: string): string | undefined;
|
|
51
|
+
/** The last line a preparation prints when its pull gave up on a registry link that kept breaking. */
|
|
52
|
+
export declare const IMAGE_DOWNLOAD_INTERRUPTED = "image_download_interrupted";
|
|
53
|
+
/** 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. */
|
|
54
|
+
export declare const KEPT_IMAGE_SKIPPED = "kept_image_skipped:";
|
|
51
55
|
/**
|
|
52
56
|
* The command carries only paths: env arrives through the staged file
|
|
53
57
|
* (mounted read-only, sourced by the entrypoint at every container start so
|
|
@@ -70,20 +74,6 @@ export declare function bootedImage(prepareStdout: string): string | undefined;
|
|
|
70
74
|
* recreated after a stop that was not this driver's; then it removes the
|
|
71
75
|
* image if a seed left it. A failure there leaves nothing serving.
|
|
72
76
|
*/
|
|
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
77
|
export declare function machineBootScripts(boot: MachineBoot): {
|
|
88
78
|
prepare: string;
|
|
89
79
|
start: string;
|
|
@@ -63,6 +63,7 @@ var sandboxDriverCredentialFields = {
|
|
|
63
63
|
{ key: "worker_url", label: "Worker URL" }
|
|
64
64
|
],
|
|
65
65
|
boat: [{ key: "api_key", label: "API Key", secret: true }],
|
|
66
|
+
agent37: [{ key: "api_key", label: "API Key", secret: true }],
|
|
66
67
|
docker: [{ key: "image", label: "Image" }]
|
|
67
68
|
};
|
|
68
69
|
var sandboxDriverLabels = {
|
|
@@ -70,6 +71,7 @@ var sandboxDriverLabels = {
|
|
|
70
71
|
vercel: "Vercel",
|
|
71
72
|
cloudflare: "Cloudflare",
|
|
72
73
|
boat: "Boat",
|
|
74
|
+
agent37: "Agent37",
|
|
73
75
|
docker: "Docker"
|
|
74
76
|
};
|
|
75
77
|
|
|
@@ -143,6 +145,10 @@ var WORKSPACE_RUNTIME_PUBLIC_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
|
143
145
|
function workspaceRuntimeBootSecrets(env) {
|
|
144
146
|
return Object.entries(env).flatMap(([key, value]) => WORKSPACE_RUNTIME_PUBLIC_ENV_KEYS.has(key) ? [] : [value]);
|
|
145
147
|
}
|
|
148
|
+
var REDACTED_VALUE_MIN_LENGTH = 8;
|
|
149
|
+
function redactWorkspaceRuntimeBootSecrets(text, secrets) {
|
|
150
|
+
return secrets.filter((secret) => secret.length >= REDACTED_VALUE_MIN_LENGTH).reduce((output, secret) => output.split(secret).join("[redacted]"), text);
|
|
151
|
+
}
|
|
146
152
|
function workspaceRuntimeBootEnv(input) {
|
|
147
153
|
assertWorkspaceRuntimeIdentityEnv(input.env);
|
|
148
154
|
const env = {
|
|
@@ -194,6 +200,10 @@ var SERVED_WORKSPACE_RUNTIME_FLOOR = {
|
|
|
194
200
|
image: "ghcr.io/kyashrathore/claxedo-sandbox:workspace-runtime-0-10-1-40706b7356-agent-plugins-v8"
|
|
195
201
|
};
|
|
196
202
|
var MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION = SERVED_WORKSPACE_RUNTIME_FLOOR.version;
|
|
203
|
+
var SANDBOX_IMAGE_RUNTIME_VERSION_FILE = "opt/workspace-runtime/workspace-runtime-version";
|
|
204
|
+
function servedRuntimeVersionTest(file) {
|
|
205
|
+
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} ]`;
|
|
206
|
+
}
|
|
197
207
|
|
|
198
208
|
// src/image-name.ts
|
|
199
209
|
function assertSandboxImageReference(image) {
|
|
@@ -214,6 +224,27 @@ var boatBilling = {
|
|
|
214
224
|
large: { size: "large", vcpu: 8, memoryGb: 16, usdPerHour: 0.072 }
|
|
215
225
|
}
|
|
216
226
|
};
|
|
227
|
+
var AGENT37_USD_PER_MONTH = { vcpu: 0.8, memoryGb: 0.7, diskGb: 0.09 };
|
|
228
|
+
var AGENT37_MONTH_HOURS = 730;
|
|
229
|
+
var agent37MachineShapes = {
|
|
230
|
+
small: { cpu: 2, memory: 4, disk: 10 },
|
|
231
|
+
default: { cpu: 4, memory: 8, disk: 20 },
|
|
232
|
+
large: { cpu: 4, memory: 16, disk: 40 }
|
|
233
|
+
};
|
|
234
|
+
function agent37Rate(shape) {
|
|
235
|
+
const usdPerMonth = shape.cpu * AGENT37_USD_PER_MONTH.vcpu + shape.memory * AGENT37_USD_PER_MONTH.memoryGb + shape.disk * AGENT37_USD_PER_MONTH.diskGb;
|
|
236
|
+
return { size: `${shape.cpu} vCPU / ${shape.memory} GB`, vcpu: shape.cpu, memoryGb: shape.memory, usdPerHour: usdPerMonth / AGENT37_MONTH_HOURS };
|
|
237
|
+
}
|
|
238
|
+
var agent37Billing = {
|
|
239
|
+
source: { url: "https://www.agent37.com/docs/agents-api/billing", readOn: "2026-11-11" },
|
|
240
|
+
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.",
|
|
241
|
+
rates: {
|
|
242
|
+
small: agent37Rate(agent37MachineShapes.small),
|
|
243
|
+
default: agent37Rate(agent37MachineShapes.default),
|
|
244
|
+
large: agent37Rate(agent37MachineShapes.large)
|
|
245
|
+
},
|
|
246
|
+
excludes: `the disk of a stopped instance, billed at $${AGENT37_USD_PER_MONTH.diskGb} per GB a month until the instance is deleted`
|
|
247
|
+
};
|
|
217
248
|
var CLOUDFLARE_USD_PER_GIB_SECOND_MEMORY = 25e-7;
|
|
218
249
|
var CLOUDFLARE_USD_PER_GB_SECOND_DISK = 7e-8;
|
|
219
250
|
var CLOUDFLARE_USD_PER_VCPU_SECOND_ACTIVE = 2e-5;
|
|
@@ -299,8 +330,8 @@ var sandboxDriverCatalog = {
|
|
|
299
330
|
hostWatchesIdle: true,
|
|
300
331
|
targetAccess: "relay",
|
|
301
332
|
secretBrokering: "native",
|
|
302
|
-
//
|
|
303
|
-
egressControl: "
|
|
333
|
+
// The sandbox Worker starts the container without internet and routes each allowed host through itself; a Worker that cannot confirm that refuses the start.
|
|
334
|
+
egressControl: "hosts",
|
|
304
335
|
// A container snapshot holds the whole root filesystem; the platform keeps it 30 days past its last restore and offers no deletion.
|
|
305
336
|
persistence: {
|
|
306
337
|
resume: "replacement-restore",
|
|
@@ -338,6 +369,33 @@ var sandboxDriverCatalog = {
|
|
|
338
369
|
machineClasses: ["small", "default", "large"]
|
|
339
370
|
}
|
|
340
371
|
},
|
|
372
|
+
agent37: {
|
|
373
|
+
id: "agent37",
|
|
374
|
+
label: sandboxDriverLabels.agent37,
|
|
375
|
+
credentialFields: sandboxDriverCredentialFields.agent37,
|
|
376
|
+
runtimeNetwork: { controlPlane: "direct", relay: "configured" },
|
|
377
|
+
runtimeEnv: "callback",
|
|
378
|
+
preparedImage: true,
|
|
379
|
+
billing: agent37Billing,
|
|
380
|
+
metadata: {
|
|
381
|
+
driverRunsIn: ["worker", "node"],
|
|
382
|
+
hostStopBehavior: "suspends-host",
|
|
383
|
+
hostResumeBehavior: "same-host",
|
|
384
|
+
targetAccess: "relay",
|
|
385
|
+
secretBrokering: "none",
|
|
386
|
+
// An instance's network policy lists domains, reached over HTTPS on 443 alone, and IPv4 ranges; private ranges stay blocked whatever it lists.
|
|
387
|
+
egressControl: "hosts-and-cidrs",
|
|
388
|
+
persistence: {
|
|
389
|
+
resume: "same-sandbox",
|
|
390
|
+
capture: "none",
|
|
391
|
+
clone: false,
|
|
392
|
+
captureSource: "not-applicable",
|
|
393
|
+
retention: "provider-managed",
|
|
394
|
+
restoreMount: "same-resource"
|
|
395
|
+
},
|
|
396
|
+
machineClasses: ["small", "default", "large"]
|
|
397
|
+
}
|
|
398
|
+
},
|
|
341
399
|
docker: {
|
|
342
400
|
id: "docker",
|
|
343
401
|
label: sandboxDriverLabels.docker,
|
|
@@ -371,10 +429,8 @@ function isRecord(value) {
|
|
|
371
429
|
function asRecord(value) {
|
|
372
430
|
return isRecord(value) ? value : void 0;
|
|
373
431
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
function num(input) {
|
|
377
|
-
return typeof input === "number" ? input : void 0;
|
|
432
|
+
function asNumber(value) {
|
|
433
|
+
return typeof value === "number" ? value : void 0;
|
|
378
434
|
}
|
|
379
435
|
|
|
380
436
|
// ../claxedo-helpers/dist/deadline.mjs
|
|
@@ -410,10 +466,10 @@ var BoatApiError = class extends Error {
|
|
|
410
466
|
}
|
|
411
467
|
/** Boat answers 401 or 403 to the API key itself, never to a sandbox it was asked about. */
|
|
412
468
|
providerKeyRefused;
|
|
413
|
-
/** Create, fork and resume each count as one start (Boat's own refusal
|
|
469
|
+
/** Create, fork and resume each count as one start (Boat's own refusal). */
|
|
414
470
|
providerRateLimited;
|
|
415
471
|
startLimit;
|
|
416
|
-
/** The state Boat's refusal gives the sandbox it refused, e.g. `archived` on a 400 `stop_failed
|
|
472
|
+
/** The state Boat's refusal gives the sandbox it refused, e.g. `archived` on a 400 `stop_failed`. */
|
|
417
473
|
sandboxState;
|
|
418
474
|
};
|
|
419
475
|
function startCount(value) {
|
|
@@ -439,7 +495,7 @@ function startLimitOf(body) {
|
|
|
439
495
|
function invalid() {
|
|
440
496
|
throw new BoatApiError("Boat API returned an invalid response", "invalid_response");
|
|
441
497
|
}
|
|
442
|
-
function
|
|
498
|
+
function responseString(value) {
|
|
443
499
|
return typeof value === "string" ? value : invalid();
|
|
444
500
|
}
|
|
445
501
|
function requiredFlag(value) {
|
|
@@ -453,7 +509,7 @@ function isSandboxState(value) {
|
|
|
453
509
|
}
|
|
454
510
|
function sandbox(body) {
|
|
455
511
|
const value = asRecord(body.sandbox) ?? invalid();
|
|
456
|
-
const id =
|
|
512
|
+
const id = responseString(value.id);
|
|
457
513
|
if (!/^bx_[23456789abcdefghjkmnpqrstuvwxyz]{8}$/.test(id) || !isSandboxState(value.state)) invalid();
|
|
458
514
|
return { id, state: value.state, hydrated: value.hydrated === true };
|
|
459
515
|
}
|
|
@@ -541,7 +597,7 @@ function createBoatClient(options) {
|
|
|
541
597
|
},
|
|
542
598
|
/**
|
|
543
599
|
* `ttlSeconds` sets the auto-stop deadline to now plus that many seconds; `null` removes it.
|
|
544
|
-
* Boat's reference example answers `sandbox.info`; the API answers `sandbox.updated
|
|
600
|
+
* Boat's reference example answers `sandbox.info`; the API answers `sandbox.updated`.
|
|
545
601
|
*/
|
|
546
602
|
async update(id, input) {
|
|
547
603
|
const value = sandbox(await api(path(id), ["sandbox.updated", "sandbox.info"], { method: "PATCH", body: input }));
|
|
@@ -576,8 +632,8 @@ function createBoatClient(options) {
|
|
|
576
632
|
const body = await api(`${path(id)}/commands`, ["command.finished"], { method: "POST", body: input, timeoutMs });
|
|
577
633
|
return {
|
|
578
634
|
success: requiredFlag(body.success),
|
|
579
|
-
stdout:
|
|
580
|
-
stderr:
|
|
635
|
+
stdout: responseString(body.stdout),
|
|
636
|
+
stderr: responseString(body.stderr),
|
|
581
637
|
exitCode: commandExitCode(body.exitCode),
|
|
582
638
|
timedOut: requiredFlag(body.timedOut)
|
|
583
639
|
};
|
|
@@ -595,7 +651,7 @@ function driverErrorSignals(err) {
|
|
|
595
651
|
return {
|
|
596
652
|
// `response.status` is the axios shape; `statusCode` is what SDK-native
|
|
597
653
|
// error classes expose instead.
|
|
598
|
-
status:
|
|
654
|
+
status: asNumber(nested.status) ?? asNumber(shaped.status) ?? asNumber(shaped.statusCode),
|
|
599
655
|
text: `${textOf(shaped.code)} ${textOf(shaped.name)} ${textOf(shaped.message)}`.toLowerCase()
|
|
600
656
|
};
|
|
601
657
|
}
|
|
@@ -658,12 +714,11 @@ function bootedImageFile(staged) {
|
|
|
658
714
|
function bootedImage(prepareStdout) {
|
|
659
715
|
return prepareStdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).pop();
|
|
660
716
|
}
|
|
661
|
-
var VERSION_FILE = "opt/workspace-runtime/workspace-runtime-version";
|
|
662
717
|
var stubLabel = `docker image inspect --format '{{index .Config.Labels "${IMAGE_LABEL}"}}' ${STUB_IMAGE} 2>/dev/null`;
|
|
663
718
|
var treesFunctions = [
|
|
664
719
|
`trees_of() { printf '%s/${PERSISTENT_ROOT}/image-%s' "$(pwd)" "$(printf %s "$1" | sed 's/[^A-Za-z0-9._-]/-/g')"; }`,
|
|
665
720
|
`seeded() { [ -f "$1/${IMAGE_TREES_COMPLETE_MARKER}" ] && [ -f "$1/${STUB_IMAGE_ARCHIVE}" ] && [ -f "$1/${IMAGE_ENV_FILE}" ]; }`,
|
|
666
|
-
`served() {
|
|
721
|
+
`served() { ${servedRuntimeVersionTest(`"$1/${SANDBOX_IMAGE_RUNTIME_VERSION_FILE}"`)}; }`
|
|
667
722
|
].join(" && ");
|
|
668
723
|
var IMAGE_DOWNLOAD_INTERRUPTED = "image_download_interrupted";
|
|
669
724
|
var KEPT_IMAGE_SKIPPED = "kept_image_skipped:";
|
|
@@ -685,7 +740,7 @@ function machineBootScripts(boot) {
|
|
|
685
740
|
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
741
|
const seedNamed = `{ docker image inspect ${named} >/dev/null 2>&1 || pull_resuming ${named}; } && { [ -f ${trees}/${IMAGE_TREES_COMPLETE_MARKER} ] || { ${seedTrees}; }; } && ${seedStub}`;
|
|
687
742
|
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")/${
|
|
743
|
+
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`;
|
|
689
744
|
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
745
|
const prepare = [
|
|
691
746
|
`chmod 600 ${boot.staged.env}`,
|
|
@@ -709,6 +764,13 @@ function machineBootScripts(boot) {
|
|
|
709
764
|
};
|
|
710
765
|
}
|
|
711
766
|
|
|
767
|
+
// ../claxedo-helpers/dist/index.mjs
|
|
768
|
+
function sleep(ms) {
|
|
769
|
+
return new Promise((resolve) => {
|
|
770
|
+
setTimeout(resolve, ms);
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
|
|
712
774
|
// src/drivers/boat.ts
|
|
713
775
|
var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
|
|
714
776
|
var DEFAULT_WORKSPACE_DIR = "/workspace";
|
|
@@ -724,7 +786,6 @@ var BOOT_TTL_SECONDS = 30 * 60;
|
|
|
724
786
|
var CONTAINER_STOP_GRACE_SECONDS = 30;
|
|
725
787
|
var HOST_RELAY_NOT_STOPPED = "The sandbox could not stop the localhost relay of this port. Try again.";
|
|
726
788
|
var STDERR_TAIL_CHARS = 600;
|
|
727
|
-
var REDACTED_VALUE_MIN_LENGTH = 8;
|
|
728
789
|
var READY_STATES = /* @__PURE__ */ new Set(["ready", "idle", "running"]);
|
|
729
790
|
var PENDING_STATES = /* @__PURE__ */ new Set(["init", "provisioning", "provisioned", "cloning"]);
|
|
730
791
|
var TRANSIENT_MARKERS = ["transport_failed", "idempotency_in_progress"];
|
|
@@ -744,14 +805,8 @@ function archivedOrThrow(err) {
|
|
|
744
805
|
if (err instanceof BoatApiError && err.code === "stop_failed" && err.sandboxState === "archived") return;
|
|
745
806
|
hostGoneOrThrow(err);
|
|
746
807
|
}
|
|
747
|
-
function sleep(ms) {
|
|
748
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
749
|
-
}
|
|
750
|
-
function scrubbed(text, secrets) {
|
|
751
|
-
return secrets.filter((secret) => secret.length >= REDACTED_VALUE_MIN_LENGTH).reduce((output, secret) => output.split(secret).join("[redacted]"), text);
|
|
752
|
-
}
|
|
753
808
|
function scrubbedTail(text, secrets) {
|
|
754
|
-
return
|
|
809
|
+
return redactWorkspaceRuntimeBootSecrets(text, secrets).trim().slice(-STDERR_TAIL_CHARS);
|
|
755
810
|
}
|
|
756
811
|
var BoatDriverError = class extends Error {
|
|
757
812
|
constructor(message) {
|
|
@@ -861,7 +916,7 @@ function createBoatSandboxDriver(options) {
|
|
|
861
916
|
command: `docker inspect --format 'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}}' ${shell(containerName)}; docker logs --tail 20 ${shell(containerName)} 2>&1`,
|
|
862
917
|
timeoutSeconds: 10
|
|
863
918
|
});
|
|
864
|
-
diagnostic = runtimeBootDiagnostic(
|
|
919
|
+
diagnostic = runtimeBootDiagnostic(redactWorkspaceRuntimeBootSecrets(`${result.stdout}
|
|
865
920
|
${result.stderr}`, secrets), STDERR_TAIL_CHARS);
|
|
866
921
|
} catch {
|
|
867
922
|
diagnostic = "container diagnostics unavailable";
|
|
@@ -1055,13 +1110,13 @@ var STATUSES = ["saving", "ready", "failed"];
|
|
|
1055
1110
|
function namedSnapshot(value) {
|
|
1056
1111
|
const snapshot = asRecord(value) ?? invalid();
|
|
1057
1112
|
const status = STATUSES.find((known) => known === snapshot.status) ?? invalid();
|
|
1058
|
-
return { name:
|
|
1113
|
+
return { name: responseString(snapshot.name), status, createdAt: responseString(snapshot.createdAt) };
|
|
1059
1114
|
}
|
|
1060
1115
|
function share(value) {
|
|
1061
1116
|
const entry = asRecord(value) ?? invalid();
|
|
1062
|
-
const status =
|
|
1117
|
+
const status = responseString(entry.status);
|
|
1063
1118
|
if (status !== "active" && status !== "revoked") invalid();
|
|
1064
|
-
return { id:
|
|
1119
|
+
return { id: responseString(entry.id), status, createdAt: responseString(entry.createdAt) };
|
|
1065
1120
|
}
|
|
1066
1121
|
function list(value, item) {
|
|
1067
1122
|
return Array.isArray(value) ? value.map(item) : invalid();
|
|
@@ -1092,7 +1147,7 @@ function createBoatNamedSnapshotClient(options) {
|
|
|
1092
1147
|
/** Boat shows the code only in this answer and keeps a hash of it. */
|
|
1093
1148
|
async share(name) {
|
|
1094
1149
|
const body = await api(`${path(name)}/shares`, ["snapshot.share.created"], { method: "POST" });
|
|
1095
|
-
return { shareCode:
|
|
1150
|
+
return { shareCode: responseString(body.shareCode), share: share(body.share) };
|
|
1096
1151
|
},
|
|
1097
1152
|
async shares(name) {
|
|
1098
1153
|
return list((await api(`${path(name)}/shares`, ["snapshot.share.list"])).shares, share);
|
|
@@ -1111,9 +1166,6 @@ var DEFAULT_POLL_INTERVAL_MS = 2e3;
|
|
|
1111
1166
|
var DEFAULT_HYDRATION_TIMEOUT_MS = 10 * 6e4;
|
|
1112
1167
|
var DEFAULT_STOP_TIMEOUT_MS = 10 * 6e4;
|
|
1113
1168
|
var DEFAULT_SAVE_TIMEOUT_MS = 10 * 6e4;
|
|
1114
|
-
function sleep2(ms) {
|
|
1115
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1116
|
-
}
|
|
1117
1169
|
async function boatRuntimeTemplateName(image, prefix = DEFAULT_PREFIX) {
|
|
1118
1170
|
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(image)));
|
|
1119
1171
|
const name = `${prefix}${Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("").slice(0, NAME_HASH_CHARS)}`;
|
|
@@ -1135,7 +1187,7 @@ async function pollBoat(label, timeoutMs, intervalMs, read) {
|
|
|
1135
1187
|
const value = await read();
|
|
1136
1188
|
if (value !== void 0) return value;
|
|
1137
1189
|
if (Date.now() >= deadline) throw new Error(`${label} did not happen within ${Math.round(timeoutMs / 1e3)} s`);
|
|
1138
|
-
await
|
|
1190
|
+
await sleep(intervalMs);
|
|
1139
1191
|
}
|
|
1140
1192
|
}
|
|
1141
1193
|
async function ensureBoatRuntimeTemplate(options) {
|
|
@@ -1183,7 +1235,7 @@ async function buildTemplate(options, name) {
|
|
|
1183
1235
|
const lease = sandboxLease({ workspaceId, homeRegion: "auto", driver: "boat", epoch, sandboxId });
|
|
1184
1236
|
const result = sandboxId ? await driver.resumeHost({ lease, ensure: ensure(epoch) }) : await driver.ensureHost(ensure(epoch));
|
|
1185
1237
|
if (!("provisioning" in result)) return result;
|
|
1186
|
-
await
|
|
1238
|
+
await sleep(result.retryAfterMs);
|
|
1187
1239
|
}
|
|
1188
1240
|
};
|
|
1189
1241
|
const archived = (id) => pollBoat(`Boat ${id} stop`, options.stopTimeoutMs ?? DEFAULT_STOP_TIMEOUT_MS, interval, async () => (await sandboxes.get(id)).state === "archived" ? true : void 0);
|
package/dist/drivers/boat.d.ts
CHANGED
|
@@ -22,12 +22,7 @@ export type BoatSandboxDriverOptions = {
|
|
|
22
22
|
workspaceDir?: string;
|
|
23
23
|
nativeHarness?: string;
|
|
24
24
|
controlEnv?: WorkspaceRuntimeControlEnv;
|
|
25
|
-
/** Container name used for the runtime container inside the sandbox. */
|
|
26
25
|
containerName?: string;
|
|
27
|
-
/**
|
|
28
|
-
* Optional `docker login` before the pull, for a private runtime registry.
|
|
29
|
-
* Omit for public images.
|
|
30
|
-
*/
|
|
31
26
|
registryAuth?: {
|
|
32
27
|
server: string;
|
|
33
28
|
username: string;
|
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
|
|
|
@@ -111,6 +113,10 @@ var WORKSPACE_RUNTIME_PUBLIC_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
|
111
113
|
function workspaceRuntimeBootSecrets(env) {
|
|
112
114
|
return Object.entries(env).flatMap(([key, value]) => WORKSPACE_RUNTIME_PUBLIC_ENV_KEYS.has(key) ? [] : [value]);
|
|
113
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
|
+
}
|
|
114
120
|
function workspaceRuntimeBootEnv(input) {
|
|
115
121
|
assertWorkspaceRuntimeIdentityEnv(input.env);
|
|
116
122
|
const env = {
|
|
@@ -162,6 +168,10 @@ var SERVED_WORKSPACE_RUNTIME_FLOOR = {
|
|
|
162
168
|
image: "ghcr.io/kyashrathore/claxedo-sandbox:workspace-runtime-0-10-1-40706b7356-agent-plugins-v8"
|
|
163
169
|
};
|
|
164
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
|
+
}
|
|
165
175
|
|
|
166
176
|
// src/image-name.ts
|
|
167
177
|
function assertSandboxImageReference(image) {
|
|
@@ -182,6 +192,27 @@ var boatBilling = {
|
|
|
182
192
|
large: { size: "large", vcpu: 8, memoryGb: 16, usdPerHour: 0.072 }
|
|
183
193
|
}
|
|
184
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
|
+
};
|
|
185
216
|
var CLOUDFLARE_USD_PER_GIB_SECOND_MEMORY = 25e-7;
|
|
186
217
|
var CLOUDFLARE_USD_PER_GB_SECOND_DISK = 7e-8;
|
|
187
218
|
var CLOUDFLARE_USD_PER_VCPU_SECOND_ACTIVE = 2e-5;
|
|
@@ -267,8 +298,8 @@ var sandboxDriverCatalog = {
|
|
|
267
298
|
hostWatchesIdle: true,
|
|
268
299
|
targetAccess: "relay",
|
|
269
300
|
secretBrokering: "native",
|
|
270
|
-
//
|
|
271
|
-
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",
|
|
272
303
|
// A container snapshot holds the whole root filesystem; the platform keeps it 30 days past its last restore and offers no deletion.
|
|
273
304
|
persistence: {
|
|
274
305
|
resume: "replacement-restore",
|
|
@@ -306,6 +337,33 @@ var sandboxDriverCatalog = {
|
|
|
306
337
|
machineClasses: ["small", "default", "large"]
|
|
307
338
|
}
|
|
308
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
|
+
},
|
|
309
367
|
docker: {
|
|
310
368
|
id: "docker",
|
|
311
369
|
label: sandboxDriverLabels.docker,
|
|
@@ -339,10 +397,8 @@ function isRecord(value) {
|
|
|
339
397
|
function asRecord(value) {
|
|
340
398
|
return isRecord(value) ? value : void 0;
|
|
341
399
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
function num(input) {
|
|
345
|
-
return typeof input === "number" ? input : void 0;
|
|
400
|
+
function asNumber(value) {
|
|
401
|
+
return typeof value === "number" ? value : void 0;
|
|
346
402
|
}
|
|
347
403
|
|
|
348
404
|
// ../claxedo-helpers/dist/deadline.mjs
|
|
@@ -378,10 +434,10 @@ var BoatApiError = class extends Error {
|
|
|
378
434
|
}
|
|
379
435
|
/** Boat answers 401 or 403 to the API key itself, never to a sandbox it was asked about. */
|
|
380
436
|
providerKeyRefused;
|
|
381
|
-
/** Create, fork and resume each count as one start (Boat's own refusal
|
|
437
|
+
/** Create, fork and resume each count as one start (Boat's own refusal). */
|
|
382
438
|
providerRateLimited;
|
|
383
439
|
startLimit;
|
|
384
|
-
/** The state Boat's refusal gives the sandbox it refused, e.g. `archived` on a 400 `stop_failed
|
|
440
|
+
/** The state Boat's refusal gives the sandbox it refused, e.g. `archived` on a 400 `stop_failed`. */
|
|
385
441
|
sandboxState;
|
|
386
442
|
};
|
|
387
443
|
function startCount(value) {
|
|
@@ -407,7 +463,7 @@ function startLimitOf(body) {
|
|
|
407
463
|
function invalid() {
|
|
408
464
|
throw new BoatApiError("Boat API returned an invalid response", "invalid_response");
|
|
409
465
|
}
|
|
410
|
-
function
|
|
466
|
+
function responseString(value) {
|
|
411
467
|
return typeof value === "string" ? value : invalid();
|
|
412
468
|
}
|
|
413
469
|
function requiredFlag(value) {
|
|
@@ -421,7 +477,7 @@ function isSandboxState(value) {
|
|
|
421
477
|
}
|
|
422
478
|
function sandbox(body) {
|
|
423
479
|
const value = asRecord(body.sandbox) ?? invalid();
|
|
424
|
-
const id =
|
|
480
|
+
const id = responseString(value.id);
|
|
425
481
|
if (!/^bx_[23456789abcdefghjkmnpqrstuvwxyz]{8}$/.test(id) || !isSandboxState(value.state)) invalid();
|
|
426
482
|
return { id, state: value.state, hydrated: value.hydrated === true };
|
|
427
483
|
}
|
|
@@ -509,7 +565,7 @@ function createBoatClient(options) {
|
|
|
509
565
|
},
|
|
510
566
|
/**
|
|
511
567
|
* `ttlSeconds` sets the auto-stop deadline to now plus that many seconds; `null` removes it.
|
|
512
|
-
* Boat's reference example answers `sandbox.info`; the API answers `sandbox.updated
|
|
568
|
+
* Boat's reference example answers `sandbox.info`; the API answers `sandbox.updated`.
|
|
513
569
|
*/
|
|
514
570
|
async update(id, input) {
|
|
515
571
|
const value = sandbox(await api(path(id), ["sandbox.updated", "sandbox.info"], { method: "PATCH", body: input }));
|
|
@@ -544,8 +600,8 @@ function createBoatClient(options) {
|
|
|
544
600
|
const body = await api(`${path(id)}/commands`, ["command.finished"], { method: "POST", body: input, timeoutMs });
|
|
545
601
|
return {
|
|
546
602
|
success: requiredFlag(body.success),
|
|
547
|
-
stdout:
|
|
548
|
-
stderr:
|
|
603
|
+
stdout: responseString(body.stdout),
|
|
604
|
+
stderr: responseString(body.stderr),
|
|
549
605
|
exitCode: commandExitCode(body.exitCode),
|
|
550
606
|
timedOut: requiredFlag(body.timedOut)
|
|
551
607
|
};
|
|
@@ -563,7 +619,7 @@ function driverErrorSignals(err) {
|
|
|
563
619
|
return {
|
|
564
620
|
// `response.status` is the axios shape; `statusCode` is what SDK-native
|
|
565
621
|
// error classes expose instead.
|
|
566
|
-
status:
|
|
622
|
+
status: asNumber(nested.status) ?? asNumber(shaped.status) ?? asNumber(shaped.statusCode),
|
|
567
623
|
text: `${textOf(shaped.code)} ${textOf(shaped.name)} ${textOf(shaped.message)}`.toLowerCase()
|
|
568
624
|
};
|
|
569
625
|
}
|
|
@@ -615,12 +671,11 @@ function bootedImageFile(staged) {
|
|
|
615
671
|
function bootedImage(prepareStdout) {
|
|
616
672
|
return prepareStdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).pop();
|
|
617
673
|
}
|
|
618
|
-
var VERSION_FILE = "opt/workspace-runtime/workspace-runtime-version";
|
|
619
674
|
var stubLabel = `docker image inspect --format '{{index .Config.Labels "${IMAGE_LABEL}"}}' ${STUB_IMAGE} 2>/dev/null`;
|
|
620
675
|
var treesFunctions = [
|
|
621
676
|
`trees_of() { printf '%s/${PERSISTENT_ROOT}/image-%s' "$(pwd)" "$(printf %s "$1" | sed 's/[^A-Za-z0-9._-]/-/g')"; }`,
|
|
622
677
|
`seeded() { [ -f "$1/${IMAGE_TREES_COMPLETE_MARKER}" ] && [ -f "$1/${STUB_IMAGE_ARCHIVE}" ] && [ -f "$1/${IMAGE_ENV_FILE}" ]; }`,
|
|
623
|
-
`served() {
|
|
678
|
+
`served() { ${servedRuntimeVersionTest(`"$1/${SANDBOX_IMAGE_RUNTIME_VERSION_FILE}"`)}; }`
|
|
624
679
|
].join(" && ");
|
|
625
680
|
var IMAGE_DOWNLOAD_INTERRUPTED = "image_download_interrupted";
|
|
626
681
|
var KEPT_IMAGE_SKIPPED = "kept_image_skipped:";
|
|
@@ -642,7 +697,7 @@ function machineBootScripts(boot) {
|
|
|
642
697
|
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
698
|
const seedNamed = `{ docker image inspect ${named} >/dev/null 2>&1 || pull_resuming ${named}; } && { [ -f ${trees}/${IMAGE_TREES_COMPLETE_MARKER} ] || { ${seedTrees}; }; } && ${seedStub}`;
|
|
644
699
|
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")/${
|
|
700
|
+
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`;
|
|
646
701
|
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
702
|
const prepare = [
|
|
648
703
|
`chmod 600 ${boot.staged.env}`,
|
|
@@ -666,6 +721,13 @@ function machineBootScripts(boot) {
|
|
|
666
721
|
};
|
|
667
722
|
}
|
|
668
723
|
|
|
724
|
+
// ../claxedo-helpers/dist/index.mjs
|
|
725
|
+
function sleep(ms) {
|
|
726
|
+
return new Promise((resolve) => {
|
|
727
|
+
setTimeout(resolve, ms);
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
|
|
669
731
|
// src/drivers/boat.ts
|
|
670
732
|
function formatBoatRuntimeTemplate(template) {
|
|
671
733
|
return JSON.stringify({ image: template.image, shareCode: template.shareCode });
|
|
@@ -697,7 +759,6 @@ var BOOT_TTL_SECONDS = 30 * 60;
|
|
|
697
759
|
var CONTAINER_STOP_GRACE_SECONDS = 30;
|
|
698
760
|
var HOST_RELAY_NOT_STOPPED = "The sandbox could not stop the localhost relay of this port. Try again.";
|
|
699
761
|
var STDERR_TAIL_CHARS = 600;
|
|
700
|
-
var REDACTED_VALUE_MIN_LENGTH = 8;
|
|
701
762
|
var READY_STATES = /* @__PURE__ */ new Set(["ready", "idle", "running"]);
|
|
702
763
|
var PENDING_STATES = /* @__PURE__ */ new Set(["init", "provisioning", "provisioned", "cloning"]);
|
|
703
764
|
var TRANSIENT_MARKERS = ["transport_failed", "idempotency_in_progress"];
|
|
@@ -717,14 +778,8 @@ function archivedOrThrow(err) {
|
|
|
717
778
|
if (err instanceof BoatApiError && err.code === "stop_failed" && err.sandboxState === "archived") return;
|
|
718
779
|
hostGoneOrThrow(err);
|
|
719
780
|
}
|
|
720
|
-
function sleep(ms) {
|
|
721
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
722
|
-
}
|
|
723
|
-
function scrubbed(text, secrets) {
|
|
724
|
-
return secrets.filter((secret) => secret.length >= REDACTED_VALUE_MIN_LENGTH).reduce((output, secret) => output.split(secret).join("[redacted]"), text);
|
|
725
|
-
}
|
|
726
781
|
function scrubbedTail(text, secrets) {
|
|
727
|
-
return
|
|
782
|
+
return redactWorkspaceRuntimeBootSecrets(text, secrets).trim().slice(-STDERR_TAIL_CHARS);
|
|
728
783
|
}
|
|
729
784
|
var BoatDriverError = class extends Error {
|
|
730
785
|
constructor(message) {
|
|
@@ -834,7 +889,7 @@ function createBoatSandboxDriver(options) {
|
|
|
834
889
|
command: `docker inspect --format 'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}}' ${shell(containerName)}; docker logs --tail 20 ${shell(containerName)} 2>&1`,
|
|
835
890
|
timeoutSeconds: 10
|
|
836
891
|
});
|
|
837
|
-
diagnostic = runtimeBootDiagnostic(
|
|
892
|
+
diagnostic = runtimeBootDiagnostic(redactWorkspaceRuntimeBootSecrets(`${result.stdout}
|
|
838
893
|
${result.stderr}`, secrets), STDERR_TAIL_CHARS);
|
|
839
894
|
} catch {
|
|
840
895
|
diagnostic = "container diagnostics unavailable";
|