@claxedo/sandbox-manager 0.10.2 → 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 +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.mjs +57 -3
- 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 +888 -0
- package/dist/drivers/boat-client.d.ts +4 -4
- package/dist/drivers/boat-machine.d.ts +7 -17
- package/dist/drivers/boat-template.mjs +67 -14
- package/dist/drivers/boat.d.ts +0 -5
- package/dist/drivers/boat.mjs +67 -14
- package/dist/drivers/cloudflare.d.ts +5 -9
- package/dist/drivers/cloudflare.mjs +64 -4
- package/dist/drivers/docker.mjs +52 -2
- package/dist/drivers/local-brokering.mjs +88 -18
- package/dist/drivers/modal.mjs +52 -2
- package/dist/drivers/transient-error.d.ts +0 -1
- package/dist/drivers/vercel.mjs +56 -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/index.mjs +103 -67
- package/dist/json.d.ts +1 -10
- package/dist/lease-failure.d.ts +3 -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 +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;
|
|
@@ -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,
|
|
@@ -410,10 +468,10 @@ var BoatApiError = class extends Error {
|
|
|
410
468
|
}
|
|
411
469
|
/** Boat answers 401 or 403 to the API key itself, never to a sandbox it was asked about. */
|
|
412
470
|
providerKeyRefused;
|
|
413
|
-
/** Create, fork and resume each count as one start (Boat's own refusal
|
|
471
|
+
/** Create, fork and resume each count as one start (Boat's own refusal). */
|
|
414
472
|
providerRateLimited;
|
|
415
473
|
startLimit;
|
|
416
|
-
/** The state Boat's refusal gives the sandbox it refused, e.g. `archived` on a 400 `stop_failed
|
|
474
|
+
/** The state Boat's refusal gives the sandbox it refused, e.g. `archived` on a 400 `stop_failed`. */
|
|
417
475
|
sandboxState;
|
|
418
476
|
};
|
|
419
477
|
function startCount(value) {
|
|
@@ -541,7 +599,7 @@ function createBoatClient(options) {
|
|
|
541
599
|
},
|
|
542
600
|
/**
|
|
543
601
|
* `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
|
|
602
|
+
* Boat's reference example answers `sandbox.info`; the API answers `sandbox.updated`.
|
|
545
603
|
*/
|
|
546
604
|
async update(id, input) {
|
|
547
605
|
const value = sandbox(await api(path(id), ["sandbox.updated", "sandbox.info"], { method: "PATCH", body: input }));
|
|
@@ -658,12 +716,11 @@ function bootedImageFile(staged) {
|
|
|
658
716
|
function bootedImage(prepareStdout) {
|
|
659
717
|
return prepareStdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).pop();
|
|
660
718
|
}
|
|
661
|
-
var VERSION_FILE = "opt/workspace-runtime/workspace-runtime-version";
|
|
662
719
|
var stubLabel = `docker image inspect --format '{{index .Config.Labels "${IMAGE_LABEL}"}}' ${STUB_IMAGE} 2>/dev/null`;
|
|
663
720
|
var treesFunctions = [
|
|
664
721
|
`trees_of() { printf '%s/${PERSISTENT_ROOT}/image-%s' "$(pwd)" "$(printf %s "$1" | sed 's/[^A-Za-z0-9._-]/-/g')"; }`,
|
|
665
722
|
`seeded() { [ -f "$1/${IMAGE_TREES_COMPLETE_MARKER}" ] && [ -f "$1/${STUB_IMAGE_ARCHIVE}" ] && [ -f "$1/${IMAGE_ENV_FILE}" ]; }`,
|
|
666
|
-
`served() {
|
|
723
|
+
`served() { ${servedRuntimeVersionTest(`"$1/${SANDBOX_IMAGE_RUNTIME_VERSION_FILE}"`)}; }`
|
|
667
724
|
].join(" && ");
|
|
668
725
|
var IMAGE_DOWNLOAD_INTERRUPTED = "image_download_interrupted";
|
|
669
726
|
var KEPT_IMAGE_SKIPPED = "kept_image_skipped:";
|
|
@@ -685,7 +742,7 @@ function machineBootScripts(boot) {
|
|
|
685
742
|
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
743
|
const seedNamed = `{ docker image inspect ${named} >/dev/null 2>&1 || pull_resuming ${named}; } && { [ -f ${trees}/${IMAGE_TREES_COMPLETE_MARKER} ] || { ${seedTrees}; }; } && ${seedStub}`;
|
|
687
744
|
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")/${
|
|
745
|
+
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
746
|
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
747
|
const prepare = [
|
|
691
748
|
`chmod 600 ${boot.staged.env}`,
|
|
@@ -724,7 +781,6 @@ var BOOT_TTL_SECONDS = 30 * 60;
|
|
|
724
781
|
var CONTAINER_STOP_GRACE_SECONDS = 30;
|
|
725
782
|
var HOST_RELAY_NOT_STOPPED = "The sandbox could not stop the localhost relay of this port. Try again.";
|
|
726
783
|
var STDERR_TAIL_CHARS = 600;
|
|
727
|
-
var REDACTED_VALUE_MIN_LENGTH = 8;
|
|
728
784
|
var READY_STATES = /* @__PURE__ */ new Set(["ready", "idle", "running"]);
|
|
729
785
|
var PENDING_STATES = /* @__PURE__ */ new Set(["init", "provisioning", "provisioned", "cloning"]);
|
|
730
786
|
var TRANSIENT_MARKERS = ["transport_failed", "idempotency_in_progress"];
|
|
@@ -747,11 +803,8 @@ function archivedOrThrow(err) {
|
|
|
747
803
|
function sleep(ms) {
|
|
748
804
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
749
805
|
}
|
|
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
806
|
function scrubbedTail(text, secrets) {
|
|
754
|
-
return
|
|
807
|
+
return redactWorkspaceRuntimeBootSecrets(text, secrets).trim().slice(-STDERR_TAIL_CHARS);
|
|
755
808
|
}
|
|
756
809
|
var BoatDriverError = class extends Error {
|
|
757
810
|
constructor(message) {
|
|
@@ -861,7 +914,7 @@ function createBoatSandboxDriver(options) {
|
|
|
861
914
|
command: `docker inspect --format 'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}}' ${shell(containerName)}; docker logs --tail 20 ${shell(containerName)} 2>&1`,
|
|
862
915
|
timeoutSeconds: 10
|
|
863
916
|
});
|
|
864
|
-
diagnostic = runtimeBootDiagnostic(
|
|
917
|
+
diagnostic = runtimeBootDiagnostic(redactWorkspaceRuntimeBootSecrets(`${result.stdout}
|
|
865
918
|
${result.stderr}`, secrets), STDERR_TAIL_CHARS);
|
|
866
919
|
} catch {
|
|
867
920
|
diagnostic = "container diagnostics unavailable";
|
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,
|
|
@@ -378,10 +436,10 @@ var BoatApiError = class extends Error {
|
|
|
378
436
|
}
|
|
379
437
|
/** Boat answers 401 or 403 to the API key itself, never to a sandbox it was asked about. */
|
|
380
438
|
providerKeyRefused;
|
|
381
|
-
/** 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). */
|
|
382
440
|
providerRateLimited;
|
|
383
441
|
startLimit;
|
|
384
|
-
/** 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`. */
|
|
385
443
|
sandboxState;
|
|
386
444
|
};
|
|
387
445
|
function startCount(value) {
|
|
@@ -509,7 +567,7 @@ function createBoatClient(options) {
|
|
|
509
567
|
},
|
|
510
568
|
/**
|
|
511
569
|
* `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
|
|
570
|
+
* Boat's reference example answers `sandbox.info`; the API answers `sandbox.updated`.
|
|
513
571
|
*/
|
|
514
572
|
async update(id, input) {
|
|
515
573
|
const value = sandbox(await api(path(id), ["sandbox.updated", "sandbox.info"], { method: "PATCH", body: input }));
|
|
@@ -615,12 +673,11 @@ function bootedImageFile(staged) {
|
|
|
615
673
|
function bootedImage(prepareStdout) {
|
|
616
674
|
return prepareStdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).pop();
|
|
617
675
|
}
|
|
618
|
-
var VERSION_FILE = "opt/workspace-runtime/workspace-runtime-version";
|
|
619
676
|
var stubLabel = `docker image inspect --format '{{index .Config.Labels "${IMAGE_LABEL}"}}' ${STUB_IMAGE} 2>/dev/null`;
|
|
620
677
|
var treesFunctions = [
|
|
621
678
|
`trees_of() { printf '%s/${PERSISTENT_ROOT}/image-%s' "$(pwd)" "$(printf %s "$1" | sed 's/[^A-Za-z0-9._-]/-/g')"; }`,
|
|
622
679
|
`seeded() { [ -f "$1/${IMAGE_TREES_COMPLETE_MARKER}" ] && [ -f "$1/${STUB_IMAGE_ARCHIVE}" ] && [ -f "$1/${IMAGE_ENV_FILE}" ]; }`,
|
|
623
|
-
`served() {
|
|
680
|
+
`served() { ${servedRuntimeVersionTest(`"$1/${SANDBOX_IMAGE_RUNTIME_VERSION_FILE}"`)}; }`
|
|
624
681
|
].join(" && ");
|
|
625
682
|
var IMAGE_DOWNLOAD_INTERRUPTED = "image_download_interrupted";
|
|
626
683
|
var KEPT_IMAGE_SKIPPED = "kept_image_skipped:";
|
|
@@ -642,7 +699,7 @@ function machineBootScripts(boot) {
|
|
|
642
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"`;
|
|
643
700
|
const seedNamed = `{ docker image inspect ${named} >/dev/null 2>&1 || pull_resuming ${named}; } && { [ -f ${trees}/${IMAGE_TREES_COMPLETE_MARKER} ] || { ${seedTrees}; }; } && ${seedStub}`;
|
|
644
701
|
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")/${
|
|
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`;
|
|
646
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`;
|
|
647
704
|
const prepare = [
|
|
648
705
|
`chmod 600 ${boot.staged.env}`,
|
|
@@ -697,7 +754,6 @@ var BOOT_TTL_SECONDS = 30 * 60;
|
|
|
697
754
|
var CONTAINER_STOP_GRACE_SECONDS = 30;
|
|
698
755
|
var HOST_RELAY_NOT_STOPPED = "The sandbox could not stop the localhost relay of this port. Try again.";
|
|
699
756
|
var STDERR_TAIL_CHARS = 600;
|
|
700
|
-
var REDACTED_VALUE_MIN_LENGTH = 8;
|
|
701
757
|
var READY_STATES = /* @__PURE__ */ new Set(["ready", "idle", "running"]);
|
|
702
758
|
var PENDING_STATES = /* @__PURE__ */ new Set(["init", "provisioning", "provisioned", "cloning"]);
|
|
703
759
|
var TRANSIENT_MARKERS = ["transport_failed", "idempotency_in_progress"];
|
|
@@ -720,11 +776,8 @@ function archivedOrThrow(err) {
|
|
|
720
776
|
function sleep(ms) {
|
|
721
777
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
722
778
|
}
|
|
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
779
|
function scrubbedTail(text, secrets) {
|
|
727
|
-
return
|
|
780
|
+
return redactWorkspaceRuntimeBootSecrets(text, secrets).trim().slice(-STDERR_TAIL_CHARS);
|
|
728
781
|
}
|
|
729
782
|
var BoatDriverError = class extends Error {
|
|
730
783
|
constructor(message) {
|
|
@@ -834,7 +887,7 @@ function createBoatSandboxDriver(options) {
|
|
|
834
887
|
command: `docker inspect --format 'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}}' ${shell(containerName)}; docker logs --tail 20 ${shell(containerName)} 2>&1`,
|
|
835
888
|
timeoutSeconds: 10
|
|
836
889
|
});
|
|
837
|
-
diagnostic = runtimeBootDiagnostic(
|
|
890
|
+
diagnostic = runtimeBootDiagnostic(redactWorkspaceRuntimeBootSecrets(`${result.stdout}
|
|
838
891
|
${result.stderr}`, secrets), STDERR_TAIL_CHARS);
|
|
839
892
|
} catch {
|
|
840
893
|
diagnostic = "container diagnostics unavailable";
|
|
@@ -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;
|
|
@@ -12,6 +12,27 @@ var boatBilling = {
|
|
|
12
12
|
large: { size: "large", vcpu: 8, memoryGb: 16, usdPerHour: 0.072 }
|
|
13
13
|
}
|
|
14
14
|
};
|
|
15
|
+
var AGENT37_USD_PER_MONTH = { vcpu: 0.8, memoryGb: 0.7, diskGb: 0.09 };
|
|
16
|
+
var AGENT37_MONTH_HOURS = 730;
|
|
17
|
+
var agent37MachineShapes = {
|
|
18
|
+
small: { cpu: 2, memory: 4, disk: 10 },
|
|
19
|
+
default: { cpu: 4, memory: 8, disk: 20 },
|
|
20
|
+
large: { cpu: 4, memory: 16, disk: 40 }
|
|
21
|
+
};
|
|
22
|
+
function agent37Rate(shape) {
|
|
23
|
+
const usdPerMonth = shape.cpu * AGENT37_USD_PER_MONTH.vcpu + shape.memory * AGENT37_USD_PER_MONTH.memoryGb + shape.disk * AGENT37_USD_PER_MONTH.diskGb;
|
|
24
|
+
return { size: `${shape.cpu} vCPU / ${shape.memory} GB`, vcpu: shape.cpu, memoryGb: shape.memory, usdPerHour: usdPerMonth / AGENT37_MONTH_HOURS };
|
|
25
|
+
}
|
|
26
|
+
var agent37Billing = {
|
|
27
|
+
source: { url: "https://www.agent37.com/docs/agents-api/billing", readOn: "2026-11-11" },
|
|
28
|
+
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.",
|
|
29
|
+
rates: {
|
|
30
|
+
small: agent37Rate(agent37MachineShapes.small),
|
|
31
|
+
default: agent37Rate(agent37MachineShapes.default),
|
|
32
|
+
large: agent37Rate(agent37MachineShapes.large)
|
|
33
|
+
},
|
|
34
|
+
excludes: `the disk of a stopped instance, billed at $${AGENT37_USD_PER_MONTH.diskGb} per GB a month until the instance is deleted`
|
|
35
|
+
};
|
|
15
36
|
var CLOUDFLARE_USD_PER_GIB_SECOND_MEMORY = 25e-7;
|
|
16
37
|
var CLOUDFLARE_USD_PER_GB_SECOND_DISK = 7e-8;
|
|
17
38
|
var CLOUDFLARE_USD_PER_VCPU_SECOND_ACTIVE = 2e-5;
|
|
@@ -50,6 +71,7 @@ var sandboxDriverCredentialFields = {
|
|
|
50
71
|
{ key: "worker_url", label: "Worker URL" }
|
|
51
72
|
],
|
|
52
73
|
boat: [{ key: "api_key", label: "API Key", secret: true }],
|
|
74
|
+
agent37: [{ key: "api_key", label: "API Key", secret: true }],
|
|
53
75
|
docker: [{ key: "image", label: "Image" }]
|
|
54
76
|
};
|
|
55
77
|
var sandboxDriverLabels = {
|
|
@@ -57,6 +79,7 @@ var sandboxDriverLabels = {
|
|
|
57
79
|
vercel: "Vercel",
|
|
58
80
|
cloudflare: "Cloudflare",
|
|
59
81
|
boat: "Boat",
|
|
82
|
+
agent37: "Agent37",
|
|
60
83
|
docker: "Docker"
|
|
61
84
|
};
|
|
62
85
|
function cloudflareWorkerBaseUrl(input) {
|
|
@@ -139,8 +162,8 @@ var sandboxDriverCatalog = {
|
|
|
139
162
|
hostWatchesIdle: true,
|
|
140
163
|
targetAccess: "relay",
|
|
141
164
|
secretBrokering: "native",
|
|
142
|
-
//
|
|
143
|
-
egressControl: "
|
|
165
|
+
// The sandbox Worker starts the container without internet and routes each allowed host through itself; a Worker that cannot confirm that refuses the start.
|
|
166
|
+
egressControl: "hosts",
|
|
144
167
|
// A container snapshot holds the whole root filesystem; the platform keeps it 30 days past its last restore and offers no deletion.
|
|
145
168
|
persistence: {
|
|
146
169
|
resume: "replacement-restore",
|
|
@@ -178,6 +201,33 @@ var sandboxDriverCatalog = {
|
|
|
178
201
|
machineClasses: ["small", "default", "large"]
|
|
179
202
|
}
|
|
180
203
|
},
|
|
204
|
+
agent37: {
|
|
205
|
+
id: "agent37",
|
|
206
|
+
label: sandboxDriverLabels.agent37,
|
|
207
|
+
credentialFields: sandboxDriverCredentialFields.agent37,
|
|
208
|
+
runtimeNetwork: { controlPlane: "direct", relay: "configured" },
|
|
209
|
+
runtimeEnv: "callback",
|
|
210
|
+
preparedImage: true,
|
|
211
|
+
billing: agent37Billing,
|
|
212
|
+
metadata: {
|
|
213
|
+
driverRunsIn: ["worker", "node"],
|
|
214
|
+
hostStopBehavior: "suspends-host",
|
|
215
|
+
hostResumeBehavior: "same-host",
|
|
216
|
+
targetAccess: "relay",
|
|
217
|
+
secretBrokering: "none",
|
|
218
|
+
// An instance's network policy lists domains, reached over HTTPS on 443 alone, and IPv4 ranges; private ranges stay blocked whatever it lists.
|
|
219
|
+
egressControl: "hosts-and-cidrs",
|
|
220
|
+
persistence: {
|
|
221
|
+
resume: "same-sandbox",
|
|
222
|
+
capture: "none",
|
|
223
|
+
clone: false,
|
|
224
|
+
captureSource: "not-applicable",
|
|
225
|
+
retention: "provider-managed",
|
|
226
|
+
restoreMount: "same-resource"
|
|
227
|
+
},
|
|
228
|
+
machineClasses: ["small", "default", "large"]
|
|
229
|
+
}
|
|
230
|
+
},
|
|
181
231
|
docker: {
|
|
182
232
|
id: "docker",
|
|
183
233
|
label: sandboxDriverLabels.docker,
|
|
@@ -441,10 +491,15 @@ function createCloudflareSandboxDriver(options) {
|
|
|
441
491
|
};
|
|
442
492
|
});
|
|
443
493
|
}
|
|
494
|
+
function networkPolicy(input) {
|
|
495
|
+
if (!input.net || input.net.mode === "allow-all") return void 0;
|
|
496
|
+
return { hosts: [...new Set(input.net.hosts ?? [])] };
|
|
497
|
+
}
|
|
444
498
|
async function ensureHost(input) {
|
|
445
499
|
const sandboxId = sandboxIdFor(input.workspaceId);
|
|
446
500
|
const hostId = sandboxId;
|
|
447
501
|
const egress = egressRegistrations(input);
|
|
502
|
+
const net = networkPolicy(input);
|
|
448
503
|
const response = await call(
|
|
449
504
|
sandboxId,
|
|
450
505
|
"ensure-runtime",
|
|
@@ -457,7 +512,8 @@ function createCloudflareSandboxDriver(options) {
|
|
|
457
512
|
// Not container env — labels are control-plane metadata.
|
|
458
513
|
labels: input.labels,
|
|
459
514
|
...input.bootSource?.kind === "driver-snapshot" ? { restore: { snapshotId: input.bootSource.snapshotId }, minimumRuntimeVersion: MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION } : {},
|
|
460
|
-
...egress ? { egress } : {}
|
|
515
|
+
...egress ? { egress } : {},
|
|
516
|
+
...net ? { net } : {}
|
|
461
517
|
}
|
|
462
518
|
).catch((error) => {
|
|
463
519
|
if (error instanceof Error && error.name === "TimeoutError") return void 0;
|
|
@@ -477,6 +533,10 @@ function createCloudflareSandboxDriver(options) {
|
|
|
477
533
|
const reason = `Cloudflare ensure-runtime failed (${status}): ${text(data.error) ?? "no runtime url"}`;
|
|
478
534
|
throw status === 401 || status === 403 ? new SandboxProviderKeyRefusedError(reason) : new Error(reason);
|
|
479
535
|
}
|
|
536
|
+
if (net && asRecord(data.egress)?.contained !== true) {
|
|
537
|
+
await call(sandboxId, "stop", { epoch: input.epoch }).catch(() => void 0);
|
|
538
|
+
throw new Error("Cloudflare sandbox Worker did not confirm egress containment; deploy the current sandbox Worker (scripts/sandbox/cloudflare-worker)");
|
|
539
|
+
}
|
|
480
540
|
const targetOut = {
|
|
481
541
|
workspaceId: input.workspaceId,
|
|
482
542
|
sandboxId,
|
|
@@ -562,7 +622,7 @@ function createCloudflareSandboxDriver(options) {
|
|
|
562
622
|
hostWatchesIdle: true,
|
|
563
623
|
targetAccess: "relay",
|
|
564
624
|
secretBrokering: "native",
|
|
565
|
-
egressControl: "
|
|
625
|
+
egressControl: "hosts",
|
|
566
626
|
persistence: sandboxDriverCatalog.cloudflare.metadata.persistence
|
|
567
627
|
},
|
|
568
628
|
ensureHost,
|