@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
|
@@ -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,
|
|
@@ -204,6 +254,17 @@ var sandboxDriverCatalog = {
|
|
|
204
254
|
}
|
|
205
255
|
};
|
|
206
256
|
|
|
257
|
+
// ../claxedo-helpers/dist/deadline.mjs
|
|
258
|
+
async function withDeadlineSignal(timeoutMs, work, parent) {
|
|
259
|
+
const deadline = new AbortController();
|
|
260
|
+
const timer = setTimeout(() => deadline.abort(new DOMException("The operation timed out.", "TimeoutError")), timeoutMs);
|
|
261
|
+
try {
|
|
262
|
+
return await work(parent ? AbortSignal.any([parent, deadline.signal]) : deadline.signal);
|
|
263
|
+
} finally {
|
|
264
|
+
clearTimeout(timer);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
207
268
|
// src/egress-policy.ts
|
|
208
269
|
function brokeredSecretPlaceholder(name) {
|
|
209
270
|
return `claxedo-broker:${name}`;
|
|
@@ -242,10 +303,11 @@ function isRecord(value) {
|
|
|
242
303
|
function asRecord(value) {
|
|
243
304
|
return isRecord(value) ? value : void 0;
|
|
244
305
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
306
|
+
function isNonEmptyString(value) {
|
|
307
|
+
return typeof value === "string" && value.length > 0;
|
|
308
|
+
}
|
|
309
|
+
function nonEmptyString(value) {
|
|
310
|
+
return isNonEmptyString(value) ? value : void 0;
|
|
249
311
|
}
|
|
250
312
|
|
|
251
313
|
// src/runtime-env.ts
|
|
@@ -337,19 +399,6 @@ var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
|
|
|
337
399
|
var DEFAULT_TIMEOUT_MS = 45e3;
|
|
338
400
|
var DEFAULT_ENSURE_TIMEOUT_MS = 3e5;
|
|
339
401
|
var DEFAULT_SNAPSHOT_TIMEOUT_MS = 18e4;
|
|
340
|
-
function deadlineSignal(timeoutMs) {
|
|
341
|
-
const controller = new AbortController();
|
|
342
|
-
const timer = setTimeout(
|
|
343
|
-
() => controller.abort(new DOMException("The operation timed out", "TimeoutError")),
|
|
344
|
-
timeoutMs
|
|
345
|
-
);
|
|
346
|
-
return {
|
|
347
|
-
signal: controller.signal,
|
|
348
|
-
cleanup() {
|
|
349
|
-
clearTimeout(timer);
|
|
350
|
-
}
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
402
|
var RUNTIME_HOME = "/home/claxedo";
|
|
354
403
|
function sandboxIdFor(workspaceId) {
|
|
355
404
|
return `claxedo-${workspaceId}`;
|
|
@@ -381,19 +430,17 @@ function createCloudflareSandboxDriver(options) {
|
|
|
381
430
|
};
|
|
382
431
|
async function call(sandboxId, action, body, method = "POST") {
|
|
383
432
|
const url = `${base}/sandbox/${encodeURIComponent(sandboxId)}${action ? `/${action}` : ""}`;
|
|
384
|
-
const
|
|
385
|
-
|
|
433
|
+
const deadlineMs = action === "ensure-runtime" ? ensureTimeoutMs : action === "snapshot" ? snapshotTimeoutMs : timeoutMs;
|
|
434
|
+
return withDeadlineSignal(deadlineMs, async (signal) => {
|
|
386
435
|
const res = await doFetch(url, {
|
|
387
436
|
method,
|
|
388
437
|
headers,
|
|
389
438
|
...method === "POST" ? { body: JSON.stringify(body) } : {},
|
|
390
|
-
signal
|
|
439
|
+
signal
|
|
391
440
|
});
|
|
392
441
|
const data = asRecord(await res.json().catch(() => ({}))) ?? {};
|
|
393
442
|
return { status: res.status, data };
|
|
394
|
-
}
|
|
395
|
-
deadline.cleanup();
|
|
396
|
-
}
|
|
443
|
+
});
|
|
397
444
|
}
|
|
398
445
|
function workspaceDirectory(input) {
|
|
399
446
|
return input.workspaceRoot ?? workspaceDir;
|
|
@@ -441,10 +488,15 @@ function createCloudflareSandboxDriver(options) {
|
|
|
441
488
|
};
|
|
442
489
|
});
|
|
443
490
|
}
|
|
491
|
+
function networkPolicy(input) {
|
|
492
|
+
if (!input.net || input.net.mode === "allow-all") return void 0;
|
|
493
|
+
return { hosts: [...new Set(input.net.hosts ?? [])] };
|
|
494
|
+
}
|
|
444
495
|
async function ensureHost(input) {
|
|
445
496
|
const sandboxId = sandboxIdFor(input.workspaceId);
|
|
446
497
|
const hostId = sandboxId;
|
|
447
498
|
const egress = egressRegistrations(input);
|
|
499
|
+
const net = networkPolicy(input);
|
|
448
500
|
const response = await call(
|
|
449
501
|
sandboxId,
|
|
450
502
|
"ensure-runtime",
|
|
@@ -457,7 +509,8 @@ function createCloudflareSandboxDriver(options) {
|
|
|
457
509
|
// Not container env — labels are control-plane metadata.
|
|
458
510
|
labels: input.labels,
|
|
459
511
|
...input.bootSource?.kind === "driver-snapshot" ? { restore: { snapshotId: input.bootSource.snapshotId }, minimumRuntimeVersion: MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION } : {},
|
|
460
|
-
...egress ? { egress } : {}
|
|
512
|
+
...egress ? { egress } : {},
|
|
513
|
+
...net ? { net } : {}
|
|
461
514
|
}
|
|
462
515
|
).catch((error) => {
|
|
463
516
|
if (error instanceof Error && error.name === "TimeoutError") return void 0;
|
|
@@ -465,18 +518,22 @@ function createCloudflareSandboxDriver(options) {
|
|
|
465
518
|
});
|
|
466
519
|
if (!response) return { provisioning: true, retryAfterMs: 0 };
|
|
467
520
|
const { status, data } = response;
|
|
468
|
-
const runtimeUrl =
|
|
521
|
+
const runtimeUrl = nonEmptyString(data.url);
|
|
469
522
|
if (status === 503 && data.error === "workspace-runtime did not become ready") {
|
|
470
523
|
return { provisioning: true, retryAfterMs: 0 };
|
|
471
524
|
}
|
|
472
|
-
if (data.exited === true) throw new SandboxRuntimeBootError(
|
|
525
|
+
if (data.exited === true) throw new SandboxRuntimeBootError(nonEmptyString(data.error) ?? "The workspace runtime exited before it was ready");
|
|
473
526
|
if (status === 410 && isSandboxCheckpointUnusableCause(data.checkpointUnusable)) {
|
|
474
|
-
throw new SandboxCheckpointUnusableError(data.checkpointUnusable,
|
|
527
|
+
throw new SandboxCheckpointUnusableError(data.checkpointUnusable, nonEmptyString(data.error) ?? "This workspace's saved files cannot be restored; choose Start fresh.");
|
|
475
528
|
}
|
|
476
529
|
if (status >= 400 || !runtimeUrl) {
|
|
477
|
-
const reason = `Cloudflare ensure-runtime failed (${status}): ${
|
|
530
|
+
const reason = `Cloudflare ensure-runtime failed (${status}): ${nonEmptyString(data.error) ?? "no runtime url"}`;
|
|
478
531
|
throw status === 401 || status === 403 ? new SandboxProviderKeyRefusedError(reason) : new Error(reason);
|
|
479
532
|
}
|
|
533
|
+
if (net && asRecord(data.egress)?.contained !== true) {
|
|
534
|
+
await call(sandboxId, "stop", { epoch: input.epoch }).catch(() => void 0);
|
|
535
|
+
throw new Error("Cloudflare sandbox Worker did not confirm egress containment; deploy the current sandbox Worker (scripts/sandbox/cloudflare-worker)");
|
|
536
|
+
}
|
|
480
537
|
const targetOut = {
|
|
481
538
|
workspaceId: input.workspaceId,
|
|
482
539
|
sandboxId,
|
|
@@ -514,12 +571,11 @@ function createCloudflareSandboxDriver(options) {
|
|
|
514
571
|
//
|
|
515
572
|
// A missing registry route or R2 binding cannot establish an empty inventory.
|
|
516
573
|
async list() {
|
|
517
|
-
|
|
518
|
-
try {
|
|
574
|
+
return withDeadlineSignal(timeoutMs, async (signal) => {
|
|
519
575
|
const res = await doFetch(`${base}/sandboxes`, {
|
|
520
576
|
method: "GET",
|
|
521
577
|
headers,
|
|
522
|
-
signal
|
|
578
|
+
signal
|
|
523
579
|
});
|
|
524
580
|
if (res.status === 404 || res.status === 501) {
|
|
525
581
|
throw new CloudflareSandboxListingUnsupportedError(`Cloudflare sandbox Worker cannot enumerate sandboxes (${res.status}); requires the /sandboxes registry route and SANDBOX_REGISTRY binding`);
|
|
@@ -528,11 +584,11 @@ function createCloudflareSandboxDriver(options) {
|
|
|
528
584
|
if (!data) throw new Error("Invalid Cloudflare sandbox listing envelope");
|
|
529
585
|
if (data.supported === false) {
|
|
530
586
|
throw new CloudflareSandboxListingUnsupportedError(
|
|
531
|
-
|
|
587
|
+
nonEmptyString(data.error) ?? `Cloudflare sandbox Worker cannot enumerate sandboxes (${res.status}) \u2014 deploy a Worker with the /sandboxes registry route and an R2 SANDBOX_REGISTRY binding`
|
|
532
588
|
);
|
|
533
589
|
}
|
|
534
590
|
if (!res.ok) {
|
|
535
|
-
throw new Error(`Cloudflare sandbox listing failed (${res.status}): ${
|
|
591
|
+
throw new Error(`Cloudflare sandbox listing failed (${res.status}): ${nonEmptyString(data.error) ?? "unknown error"}`);
|
|
536
592
|
}
|
|
537
593
|
return registryEntries(data.sandboxes).flatMap((entry) => {
|
|
538
594
|
const sandboxId = entry.sandboxId;
|
|
@@ -551,9 +607,7 @@ function createCloudflareSandboxDriver(options) {
|
|
|
551
607
|
};
|
|
552
608
|
return [target];
|
|
553
609
|
});
|
|
554
|
-
}
|
|
555
|
-
deadline.cleanup();
|
|
556
|
-
}
|
|
610
|
+
});
|
|
557
611
|
},
|
|
558
612
|
metadata: {
|
|
559
613
|
driverRunsIn: ["worker"],
|
|
@@ -562,7 +616,7 @@ function createCloudflareSandboxDriver(options) {
|
|
|
562
616
|
hostWatchesIdle: true,
|
|
563
617
|
targetAccess: "relay",
|
|
564
618
|
secretBrokering: "native",
|
|
565
|
-
egressControl: "
|
|
619
|
+
egressControl: "hosts",
|
|
566
620
|
persistence: sandboxDriverCatalog.cloudflare.metadata.persistence
|
|
567
621
|
},
|
|
568
622
|
ensureHost,
|
|
@@ -575,14 +629,14 @@ function createCloudflareSandboxDriver(options) {
|
|
|
575
629
|
async stop(target) {
|
|
576
630
|
const { status, data } = await call(target.sandboxId, "stop", { epoch: target.epoch });
|
|
577
631
|
if (status >= 400 || data.ok !== true) {
|
|
578
|
-
throw new Error(`Cloudflare stop failed (${status}) for ${target.sandboxId}: ${
|
|
632
|
+
throw new Error(`Cloudflare stop failed (${status}) for ${target.sandboxId}: ${nonEmptyString(data.error) ?? "not stopped"}`);
|
|
579
633
|
}
|
|
580
634
|
},
|
|
581
635
|
// The sandbox's object outlives its container and keeps the snapshots it took, so a container that is not running is a stopped host, never a gone one.
|
|
582
636
|
async inspect(target) {
|
|
583
637
|
const { status, data } = await call(target.sandboxId, "inspect", {});
|
|
584
638
|
if (status < 200 || status >= 300 || typeof data.running !== "boolean") {
|
|
585
|
-
throw new Error(`Cloudflare inspect failed (${status}) for ${target.sandboxId}: ${
|
|
639
|
+
throw new Error(`Cloudflare inspect failed (${status}) for ${target.sandboxId}: ${nonEmptyString(data.error) ?? "no container state"}`);
|
|
586
640
|
}
|
|
587
641
|
return data.running ? "serving" : "stopped";
|
|
588
642
|
},
|
|
@@ -594,9 +648,9 @@ function createCloudflareSandboxDriver(options) {
|
|
|
594
648
|
},
|
|
595
649
|
async snapshot(target) {
|
|
596
650
|
const { status, data } = await call(target.sandboxId, "snapshot", {});
|
|
597
|
-
const snapshotId =
|
|
651
|
+
const snapshotId = nonEmptyString(data.snapshotId);
|
|
598
652
|
if (status >= 400 || !snapshotId) {
|
|
599
|
-
throw new Error(`Cloudflare snapshot failed (${status}): ${
|
|
653
|
+
throw new Error(`Cloudflare snapshot failed (${status}): ${nonEmptyString(data.error) ?? "no snapshot id"}`);
|
|
600
654
|
}
|
|
601
655
|
return { snapshotId };
|
|
602
656
|
}
|
package/dist/drivers/docker.mjs
CHANGED
|
@@ -138,6 +138,27 @@ var boatBilling = {
|
|
|
138
138
|
large: { size: "large", vcpu: 8, memoryGb: 16, usdPerHour: 0.072 }
|
|
139
139
|
}
|
|
140
140
|
};
|
|
141
|
+
var AGENT37_USD_PER_MONTH = { vcpu: 0.8, memoryGb: 0.7, diskGb: 0.09 };
|
|
142
|
+
var AGENT37_MONTH_HOURS = 730;
|
|
143
|
+
var agent37MachineShapes = {
|
|
144
|
+
small: { cpu: 2, memory: 4, disk: 10 },
|
|
145
|
+
default: { cpu: 4, memory: 8, disk: 20 },
|
|
146
|
+
large: { cpu: 4, memory: 16, disk: 40 }
|
|
147
|
+
};
|
|
148
|
+
function agent37Rate(shape) {
|
|
149
|
+
const usdPerMonth = shape.cpu * AGENT37_USD_PER_MONTH.vcpu + shape.memory * AGENT37_USD_PER_MONTH.memoryGb + shape.disk * AGENT37_USD_PER_MONTH.diskGb;
|
|
150
|
+
return { size: `${shape.cpu} vCPU / ${shape.memory} GB`, vcpu: shape.cpu, memoryGb: shape.memory, usdPerHour: usdPerMonth / AGENT37_MONTH_HOURS };
|
|
151
|
+
}
|
|
152
|
+
var agent37Billing = {
|
|
153
|
+
source: { url: "https://www.agent37.com/docs/agents-api/billing", readOn: "2026-11-11" },
|
|
154
|
+
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.",
|
|
155
|
+
rates: {
|
|
156
|
+
small: agent37Rate(agent37MachineShapes.small),
|
|
157
|
+
default: agent37Rate(agent37MachineShapes.default),
|
|
158
|
+
large: agent37Rate(agent37MachineShapes.large)
|
|
159
|
+
},
|
|
160
|
+
excludes: `the disk of a stopped instance, billed at $${AGENT37_USD_PER_MONTH.diskGb} per GB a month until the instance is deleted`
|
|
161
|
+
};
|
|
141
162
|
var CLOUDFLARE_USD_PER_GIB_SECOND_MEMORY = 25e-7;
|
|
142
163
|
var CLOUDFLARE_USD_PER_GB_SECOND_DISK = 7e-8;
|
|
143
164
|
var CLOUDFLARE_USD_PER_VCPU_SECOND_ACTIVE = 2e-5;
|
|
@@ -172,6 +193,7 @@ var sandboxDriverCredentialFields = {
|
|
|
172
193
|
{ key: "worker_url", label: "Worker URL" }
|
|
173
194
|
],
|
|
174
195
|
boat: [{ key: "api_key", label: "API Key", secret: true }],
|
|
196
|
+
agent37: [{ key: "api_key", label: "API Key", secret: true }],
|
|
175
197
|
docker: [{ key: "image", label: "Image" }]
|
|
176
198
|
};
|
|
177
199
|
var sandboxDriverLabels = {
|
|
@@ -179,6 +201,7 @@ var sandboxDriverLabels = {
|
|
|
179
201
|
vercel: "Vercel",
|
|
180
202
|
cloudflare: "Cloudflare",
|
|
181
203
|
boat: "Boat",
|
|
204
|
+
agent37: "Agent37",
|
|
182
205
|
docker: "Docker"
|
|
183
206
|
};
|
|
184
207
|
|
|
@@ -249,8 +272,8 @@ var sandboxDriverCatalog = {
|
|
|
249
272
|
hostWatchesIdle: true,
|
|
250
273
|
targetAccess: "relay",
|
|
251
274
|
secretBrokering: "native",
|
|
252
|
-
//
|
|
253
|
-
egressControl: "
|
|
275
|
+
// The sandbox Worker starts the container without internet and routes each allowed host through itself; a Worker that cannot confirm that refuses the start.
|
|
276
|
+
egressControl: "hosts",
|
|
254
277
|
// A container snapshot holds the whole root filesystem; the platform keeps it 30 days past its last restore and offers no deletion.
|
|
255
278
|
persistence: {
|
|
256
279
|
resume: "replacement-restore",
|
|
@@ -288,6 +311,33 @@ var sandboxDriverCatalog = {
|
|
|
288
311
|
machineClasses: ["small", "default", "large"]
|
|
289
312
|
}
|
|
290
313
|
},
|
|
314
|
+
agent37: {
|
|
315
|
+
id: "agent37",
|
|
316
|
+
label: sandboxDriverLabels.agent37,
|
|
317
|
+
credentialFields: sandboxDriverCredentialFields.agent37,
|
|
318
|
+
runtimeNetwork: { controlPlane: "direct", relay: "configured" },
|
|
319
|
+
runtimeEnv: "callback",
|
|
320
|
+
preparedImage: true,
|
|
321
|
+
billing: agent37Billing,
|
|
322
|
+
metadata: {
|
|
323
|
+
driverRunsIn: ["worker", "node"],
|
|
324
|
+
hostStopBehavior: "suspends-host",
|
|
325
|
+
hostResumeBehavior: "same-host",
|
|
326
|
+
targetAccess: "relay",
|
|
327
|
+
secretBrokering: "none",
|
|
328
|
+
// An instance's network policy lists domains, reached over HTTPS on 443 alone, and IPv4 ranges; private ranges stay blocked whatever it lists.
|
|
329
|
+
egressControl: "hosts-and-cidrs",
|
|
330
|
+
persistence: {
|
|
331
|
+
resume: "same-sandbox",
|
|
332
|
+
capture: "none",
|
|
333
|
+
clone: false,
|
|
334
|
+
captureSource: "not-applicable",
|
|
335
|
+
retention: "provider-managed",
|
|
336
|
+
restoreMount: "same-resource"
|
|
337
|
+
},
|
|
338
|
+
machineClasses: ["small", "default", "large"]
|
|
339
|
+
}
|
|
340
|
+
},
|
|
291
341
|
docker: {
|
|
292
342
|
id: "docker",
|
|
293
343
|
label: sandboxDriverLabels.docker,
|
|
@@ -318,10 +368,8 @@ var sandboxDriverCatalog = {
|
|
|
318
368
|
function isRecord(value) {
|
|
319
369
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
320
370
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
function num(input) {
|
|
324
|
-
return typeof input === "number" ? input : void 0;
|
|
371
|
+
function asNumber(value) {
|
|
372
|
+
return typeof value === "number" ? value : void 0;
|
|
325
373
|
}
|
|
326
374
|
|
|
327
375
|
// src/drivers/transient-error.ts
|
|
@@ -334,7 +382,7 @@ function driverErrorSignals(err) {
|
|
|
334
382
|
return {
|
|
335
383
|
// `response.status` is the axios shape; `statusCode` is what SDK-native
|
|
336
384
|
// error classes expose instead.
|
|
337
|
-
status:
|
|
385
|
+
status: asNumber(nested.status) ?? asNumber(shaped.status) ?? asNumber(shaped.statusCode),
|
|
338
386
|
text: `${textOf(shaped.code)} ${textOf(shaped.name)} ${textOf(shaped.message)}`.toLowerCase()
|
|
339
387
|
};
|
|
340
388
|
}
|
|
@@ -356,6 +404,18 @@ async function withDeadlineSignal(timeoutMs, work, parent) {
|
|
|
356
404
|
}
|
|
357
405
|
}
|
|
358
406
|
|
|
407
|
+
// ../claxedo-helpers/dist/index.mjs
|
|
408
|
+
function sleep(ms) {
|
|
409
|
+
return new Promise((resolve) => {
|
|
410
|
+
setTimeout(resolve, ms);
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ../claxedo-helpers/dist/env.mjs
|
|
415
|
+
function isEnabledFlag(value) {
|
|
416
|
+
return ["1", "true", "yes", "on"].includes(value?.trim().toLowerCase() ?? "");
|
|
417
|
+
}
|
|
418
|
+
|
|
359
419
|
// src/drivers/docker.ts
|
|
360
420
|
var execFileAsync = promisify(execFile);
|
|
361
421
|
var DEFAULT_OPERATION_TIMEOUT_MS = 6e4;
|
|
@@ -366,7 +426,7 @@ var DEFAULT_WORKSPACE_DIR = "/workspace";
|
|
|
366
426
|
var DOCKER_LABEL = "claxedo";
|
|
367
427
|
var AUTH_STAGE_DIR = "/tmp/claxedo-auth";
|
|
368
428
|
function dockerSandboxSyncLocalAuth(env = process.env) {
|
|
369
|
-
return
|
|
429
|
+
return isEnabledFlag(env.CLAXEDO_DOCKER_SANDBOX_SYNC_LOCAL_AUTH);
|
|
370
430
|
}
|
|
371
431
|
function dockerSandboxAuthHome(env = process.env) {
|
|
372
432
|
return trimToUndefined(env.CLAXEDO_DOCKER_SANDBOX_AUTH_HOME) ?? trimToUndefined(env.HOME) ?? os.homedir();
|
|
@@ -403,9 +463,6 @@ function dockerLocalAuthCandidates(home) {
|
|
|
403
463
|
}
|
|
404
464
|
];
|
|
405
465
|
}
|
|
406
|
-
function enabledFlag(input) {
|
|
407
|
-
return ["1", "true", "yes", "on"].includes(input?.trim().toLowerCase() ?? "");
|
|
408
|
-
}
|
|
409
466
|
function nameFor(workspaceId) {
|
|
410
467
|
const cleanId = workspaceId.toLowerCase().replace(/[^a-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
411
468
|
return `claxedo-${cleanId || "workspace"}-${Date.now().toString(36)}`.slice(0, 63);
|
|
@@ -417,9 +474,6 @@ async function docker(args, timeoutMs = DEFAULT_OPERATION_TIMEOUT_MS, env) {
|
|
|
417
474
|
...env ? { env: { ...process.env, ...env } } : {}
|
|
418
475
|
});
|
|
419
476
|
}
|
|
420
|
-
function sleep(ms) {
|
|
421
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
422
|
-
}
|
|
423
477
|
async function stageLocalAgentAuth(containerId, input) {
|
|
424
478
|
if (!input.syncLocalAuth) return;
|
|
425
479
|
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "claxedo-docker-auth-"));
|
|
@@ -472,9 +526,6 @@ function labelArgs(input) {
|
|
|
472
526
|
return Object.entries(input).flatMap(([key, value]) => ["--label", `${key}=${value}`]);
|
|
473
527
|
}
|
|
474
528
|
var TRANSIENT_MARKERS = ["timeout", "temporarily unavailable", "connection refused"];
|
|
475
|
-
function transientDriverError(err) {
|
|
476
|
-
return isTransientDriverError(err, TRANSIENT_MARKERS);
|
|
477
|
-
}
|
|
478
529
|
function createDockerSandboxDriver(options) {
|
|
479
530
|
const dockerCommand = options.docker ?? docker;
|
|
480
531
|
const runtimeCommand = options.runtimeCommand ?? DEFAULT_RUNTIME_COMMAND;
|
|
@@ -571,7 +622,7 @@ function createDockerSandboxDriver(options) {
|
|
|
571
622
|
"-lc",
|
|
572
623
|
bootScript(workspaceDirectory(input), runtimeCommand)
|
|
573
624
|
], 12e4, env).catch((err) => {
|
|
574
|
-
if (
|
|
625
|
+
if (isTransientDriverError(err, TRANSIENT_MARKERS)) return void 0;
|
|
575
626
|
throw err;
|
|
576
627
|
});
|
|
577
628
|
if (!created) return { provisioning: true, retryAfterMs: TRANSIENT_DRIVER_RETRY_MS };
|
|
@@ -585,7 +636,7 @@ function createDockerSandboxDriver(options) {
|
|
|
585
636
|
if (!sandboxId) return ensureHost(input.ensure);
|
|
586
637
|
await dockerCommand(["inspect", "-f", "{{.Id}}", sandboxId], timeoutMs);
|
|
587
638
|
await dockerCommand(["start", sandboxId], timeoutMs).catch((err) => {
|
|
588
|
-
if (
|
|
639
|
+
if (isTransientDriverError(err, TRANSIENT_MARKERS)) return void 0;
|
|
589
640
|
throw err;
|
|
590
641
|
});
|
|
591
642
|
return readyTarget(input.ensure, sandboxId, input.ensure.hostId ?? sandboxId);
|