@claxedo/sandbox-manager 0.9.0 → 0.10.1
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 +233 -26
- package/dist/checkpoint-manager.d.ts +29 -4
- package/dist/checkpoint-manager.mjs +133 -24
- package/dist/command.d.ts +9 -2
- package/dist/command.mjs +8 -8
- package/dist/contract.d.ts +687 -0
- package/dist/driver-billing.d.ts +29 -0
- package/dist/driver-catalog.d.ts +42 -7
- package/dist/driver-catalog.mjs +115 -161
- package/dist/drivers/boat-client.d.ts +95 -0
- package/dist/drivers/boat-named-snapshots.d.ts +31 -0
- package/dist/drivers/boat-template.d.ts +49 -0
- package/dist/drivers/boat-template.mjs +1196 -0
- package/dist/drivers/boat.d.ts +64 -0
- package/dist/drivers/boat.mjs +990 -0
- package/dist/drivers/cloudflare.d.ts +3 -3
- package/dist/drivers/cloudflare.mjs +208 -147
- package/dist/drivers/docker.d.ts +4 -3
- package/dist/drivers/docker.mjs +154 -126
- package/dist/drivers/local-brokering.d.ts +14 -0
- package/dist/drivers/local-brokering.mjs +1029 -0
- package/dist/drivers/modal.d.ts +2 -2
- package/dist/drivers/modal.mjs +121 -111
- package/dist/drivers/transient-error.d.ts +2 -0
- package/dist/drivers/vercel.d.ts +3 -2
- package/dist/drivers/vercel.mjs +151 -115
- package/dist/egress-policy.d.ts +134 -0
- package/dist/egress-warnings.d.ts +22 -0
- package/dist/garbage-collect.d.ts +14 -0
- package/dist/hosted-network-policy.d.ts +9 -2
- package/dist/hosted-network-policy.mjs +218 -1
- package/dist/image-name.d.ts +12 -2
- package/dist/image-name.mjs +14 -5
- package/dist/image.d.ts +1 -20
- package/dist/image.mjs +11 -88
- package/dist/index.d.ts +8 -578
- package/dist/index.mjs +1379 -320
- package/dist/lease-failure.d.ts +173 -0
- package/dist/lease-lifecycle.d.ts +65 -0
- package/dist/lease-retry-wait.d.ts +8 -0
- package/dist/lease-start.d.ts +21 -0
- package/dist/lease-types.d.ts +4 -3
- package/dist/manager.d.ts +2 -0
- package/dist/runtime-env.d.ts +4 -2
- package/dist/runtime-env.mjs +37 -2
- package/dist/runtime-snapshot.d.ts +49 -0
- package/dist/runtime-version.d.ts +9 -0
- package/dist/runtime-version.mjs +2 -0
- package/dist/service-url-exposure.d.ts +0 -1
- package/dist/service-url-exposure.mjs +0 -9
- package/dist/start-fresh.d.ts +17 -0
- package/dist/start-telemetry.d.ts +37 -0
- package/dist/stores/memory.d.ts +1 -1
- package/dist/stores/memory.mjs +63 -52
- package/package.json +26 -42
- package/dist/daytona-allow-list.d.ts +0 -16
- package/dist/daytona-allow-list.mjs +0 -11
- package/dist/drivers/box.d.ts +0 -52
- package/dist/drivers/box.mjs +0 -574
- package/dist/drivers/daytona.d.ts +0 -163
- package/dist/drivers/daytona.mjs +0 -683
- package/dist/drivers/exe.d.ts +0 -26
- package/dist/drivers/exe.mjs +0 -492
- package/dist/drivers/fetch-bridge.d.ts +0 -10
- package/dist/drivers/fetch-bridge.mjs +0 -126
- package/dist/lease-policy.d.ts +0 -52
- package/dist/lease-policy.mjs +0 -290
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { SandboxMachineClass } from "@claxedo/sandbox-contract";
|
|
2
|
+
/** One size a provider sells, as its own catalog names and prices it. */
|
|
3
|
+
export type SandboxMachineRate = {
|
|
4
|
+
/** The provider's own name for the size. */
|
|
5
|
+
size: string;
|
|
6
|
+
vcpu: number;
|
|
7
|
+
memoryGb: number;
|
|
8
|
+
usdPerHour: number;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* How a provider bills the machine a sandbox runs on, read from its published
|
|
12
|
+
* pricing. `rates.default` is also the one size of a driver that offers no
|
|
13
|
+
* choice. `excludes` names a component the hourly rate leaves out because the
|
|
14
|
+
* provider meters it on use rather than on time.
|
|
15
|
+
*/
|
|
16
|
+
export type SandboxDriverBilling = {
|
|
17
|
+
source: {
|
|
18
|
+
url: string;
|
|
19
|
+
readOn: string;
|
|
20
|
+
};
|
|
21
|
+
meter: string;
|
|
22
|
+
rates: Partial<Record<SandboxMachineClass, SandboxMachineRate>>;
|
|
23
|
+
excludes?: string;
|
|
24
|
+
};
|
|
25
|
+
export declare const boatBilling: SandboxDriverBilling;
|
|
26
|
+
export declare const cloudflareBilling: SandboxDriverBilling;
|
|
27
|
+
/** The rate a sandbox of this class runs at; a driver with one size answers it for the class it was not asked for. */
|
|
28
|
+
export declare function sandboxMachineRate(billing: SandboxDriverBilling | undefined, machineClass: SandboxMachineClass | null): SandboxMachineRate | undefined;
|
|
29
|
+
export declare function sandboxMachineCostUsd(rate: SandboxMachineRate, activeMs: number): number;
|
package/dist/driver-catalog.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { validateSandboxPersistenceCapabilities, type SandboxDriverMetadata } from "./
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
export
|
|
5
|
-
|
|
1
|
+
import { validateSandboxPersistenceCapabilities, type SandboxDriverMetadata } from "./contract";
|
|
2
|
+
import { type SandboxDriverBilling } from "./driver-billing";
|
|
3
|
+
import { sandboxDriverId, type SandboxDriverEnv, type SandboxDriverID } from "@claxedo/sandbox-contract";
|
|
4
|
+
export { sandboxDriverId };
|
|
5
|
+
export { sandboxMachineCostUsd, sandboxMachineRate, type SandboxDriverBilling, type SandboxMachineRate } from "./driver-billing";
|
|
6
|
+
export type SandboxDriverCatalogEntry<ID extends string = SandboxDriverID> = {
|
|
7
|
+
id: ID;
|
|
6
8
|
label: string;
|
|
7
9
|
metadata: SandboxDriverMetadata;
|
|
8
10
|
credentialFields: ReadonlyArray<{
|
|
@@ -10,9 +12,42 @@ export type SandboxDriverCatalogEntry = {
|
|
|
10
12
|
label: string;
|
|
11
13
|
secret?: boolean;
|
|
12
14
|
}>;
|
|
15
|
+
runtimeNetwork: {
|
|
16
|
+
controlPlane: "docker-host" | "direct";
|
|
17
|
+
relay: "configured" | "local";
|
|
18
|
+
};
|
|
19
|
+
runtimeEnv: "ensure" | "callback";
|
|
20
|
+
preparedImage: boolean;
|
|
21
|
+
/** Absent where no published rate for the machines this driver places on has been established. */
|
|
22
|
+
billing?: SandboxDriverBilling;
|
|
13
23
|
};
|
|
14
24
|
export { validateSandboxPersistenceCapabilities };
|
|
25
|
+
export declare const localBrokeringTestDriverCatalogEntry: {
|
|
26
|
+
readonly id: "local-brokering-test";
|
|
27
|
+
readonly label: "Local brokering test driver";
|
|
28
|
+
readonly credentialFields: readonly [];
|
|
29
|
+
readonly runtimeNetwork: {
|
|
30
|
+
readonly controlPlane: "direct";
|
|
31
|
+
readonly relay: "local";
|
|
32
|
+
};
|
|
33
|
+
readonly runtimeEnv: "ensure";
|
|
34
|
+
readonly preparedImage: false;
|
|
35
|
+
readonly metadata: {
|
|
36
|
+
readonly driverRunsIn: ["local"];
|
|
37
|
+
readonly hostStopBehavior: "terminates-host";
|
|
38
|
+
readonly hostResumeBehavior: "same-host";
|
|
39
|
+
readonly targetAccess: "loopback";
|
|
40
|
+
readonly secretBrokering: "native";
|
|
41
|
+
readonly egressControl: "hosts";
|
|
42
|
+
readonly persistence: {
|
|
43
|
+
readonly resume: "same-sandbox";
|
|
44
|
+
readonly capture: "none";
|
|
45
|
+
readonly clone: false;
|
|
46
|
+
readonly captureSource: "not-applicable";
|
|
47
|
+
readonly retention: "provider-managed";
|
|
48
|
+
readonly restoreMount: "same-resource";
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
};
|
|
15
52
|
export declare const sandboxDriverCatalog: Record<SandboxDriverID, SandboxDriverCatalogEntry>;
|
|
16
|
-
export declare function sandboxDriverAuth<T extends SandboxDriverID>(cfg: SandboxDriverConfig | undefined, id: T, env?: SandboxDriverEnv): SandboxDriverAuth[T] | undefined;
|
|
17
|
-
export declare function hasSandboxDriverAuth(cfg: SandboxDriverConfig | undefined, id: SandboxDriverID, env?: SandboxDriverEnv): boolean;
|
|
18
53
|
export declare function dockerSandboxDriverEnabled(env?: SandboxDriverEnv): boolean;
|
package/dist/driver-catalog.mjs
CHANGED
|
@@ -1,30 +1,4 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
var SANDBOX_MODEL_PROVIDER_HOSTS = [
|
|
3
|
-
"api.anthropic.com",
|
|
4
|
-
"api.openai.com",
|
|
5
|
-
"generativelanguage.googleapis.com",
|
|
6
|
-
"openrouter.ai",
|
|
7
|
-
// Model catalog + engine config, fetched on runner startup.
|
|
8
|
-
"models.dev",
|
|
9
|
-
"opencode.ai"
|
|
10
|
-
];
|
|
11
|
-
var SANDBOX_PACKAGE_REGISTRY_HOSTS = [
|
|
12
|
-
"registry.npmjs.org",
|
|
13
|
-
"nodejs.org",
|
|
14
|
-
"pypi.org",
|
|
15
|
-
"files.pythonhosted.org",
|
|
16
|
-
"crates.io",
|
|
17
|
-
"static.crates.io",
|
|
18
|
-
"index.crates.io",
|
|
19
|
-
"proxy.golang.org",
|
|
20
|
-
"sum.golang.org"
|
|
21
|
-
];
|
|
22
|
-
var SANDBOX_BASELINE_EGRESS_HOSTS = [
|
|
23
|
-
...SANDBOX_MODEL_PROVIDER_HOSTS,
|
|
24
|
-
...SANDBOX_PACKAGE_REGISTRY_HOSTS
|
|
25
|
-
];
|
|
26
|
-
|
|
27
|
-
// src/index.ts
|
|
1
|
+
// src/contract.ts
|
|
28
2
|
function validateSandboxPersistenceCapabilities(input) {
|
|
29
3
|
if (input.resume === "replacement-restore" && input.capture === "none") {
|
|
30
4
|
return { valid: false, reason: "replacement restore requires a capture source" };
|
|
@@ -38,34 +12,43 @@ function validateSandboxPersistenceCapabilities(input) {
|
|
|
38
12
|
if (input.capture === "same-resource" && input.restoreMount !== "same-resource") {
|
|
39
13
|
return { valid: false, reason: "same-resource capture requires same-resource restore" };
|
|
40
14
|
}
|
|
41
|
-
if (input.restoreMount === "copy-on-write" && input.capture !== "directories") {
|
|
42
|
-
return { valid: false, reason: "copy-on-write restore requires directory capture" };
|
|
43
|
-
}
|
|
44
15
|
return { valid: true };
|
|
45
16
|
}
|
|
46
17
|
|
|
47
|
-
// src/
|
|
48
|
-
var
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
18
|
+
// src/driver-billing.ts
|
|
19
|
+
var HOUR_SECONDS = 3600;
|
|
20
|
+
var boatBilling = {
|
|
21
|
+
source: { url: "https://docs.boat.dev/pricing", readOn: "2026-10-12" },
|
|
22
|
+
meter: "Boat bills machine time per second while a sandbox runs, at an hourly rate per size; a stopped sandbox costs nothing.",
|
|
23
|
+
rates: {
|
|
24
|
+
small: { size: "small", vcpu: 2, memoryGb: 4, usdPerHour: 0.018 },
|
|
25
|
+
default: { size: "default", vcpu: 4, memoryGb: 8, usdPerHour: 0.036 },
|
|
26
|
+
large: { size: "large", vcpu: 8, memoryGb: 16, usdPerHour: 0.072 }
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var CLOUDFLARE_USD_PER_GIB_SECOND_MEMORY = 25e-7;
|
|
30
|
+
var CLOUDFLARE_USD_PER_GB_SECOND_DISK = 7e-8;
|
|
31
|
+
var CLOUDFLARE_USD_PER_VCPU_SECOND_ACTIVE = 2e-5;
|
|
32
|
+
var CLOUDFLARE_STANDARD_1 = { size: "standard-1", vcpu: 0.5, memoryGb: 4, diskGb: 8 };
|
|
33
|
+
var cloudflareBilling = {
|
|
34
|
+
source: { url: "https://developers.cloudflare.com/containers/pricing/", readOn: "2026-10-12" },
|
|
35
|
+
meter: "Cloudflare bills a container for every 10 ms it runs: memory at $0.0000025 per GiB-second and disk at $0.00000007 per GB-second for the whole run, plus CPU at $0.00002 per vCPU-second of active use.",
|
|
36
|
+
rates: {
|
|
37
|
+
default: {
|
|
38
|
+
size: CLOUDFLARE_STANDARD_1.size,
|
|
39
|
+
vcpu: CLOUDFLARE_STANDARD_1.vcpu,
|
|
40
|
+
memoryGb: CLOUDFLARE_STANDARD_1.memoryGb,
|
|
41
|
+
usdPerHour: (CLOUDFLARE_STANDARD_1.memoryGb * CLOUDFLARE_USD_PER_GIB_SECOND_MEMORY + CLOUDFLARE_STANDARD_1.diskGb * CLOUDFLARE_USD_PER_GB_SECOND_DISK) * HOUR_SECONDS
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
excludes: `active CPU, billed at $${CLOUDFLARE_USD_PER_VCPU_SECOND_ACTIVE} per vCPU-second and not measured by Claxedo, and the monthly allowance a Workers Paid plan includes`
|
|
45
|
+
};
|
|
46
|
+
function sandboxMachineRate(billing, machineClass) {
|
|
47
|
+
if (!billing) return void 0;
|
|
48
|
+
return billing.rates[machineClass ?? "default"] ?? (Object.keys(billing.rates).length === 1 ? billing.rates.default : void 0);
|
|
66
49
|
}
|
|
67
|
-
function
|
|
68
|
-
return
|
|
50
|
+
function sandboxMachineCostUsd(rate, activeMs) {
|
|
51
|
+
return rate.usdPerHour * activeMs / (HOUR_SECONDS * 1e3);
|
|
69
52
|
}
|
|
70
53
|
|
|
71
54
|
// ../sandbox-contract/dist/index.mjs
|
|
@@ -78,10 +61,8 @@ function trimToUndefined(value) {
|
|
|
78
61
|
const trimmed = value.trim();
|
|
79
62
|
return trimmed.length > 0 ? trimmed : void 0;
|
|
80
63
|
}
|
|
81
|
-
var sandboxDriverIds = ["
|
|
64
|
+
var sandboxDriverIds = ["modal", "vercel", "cloudflare", "boat", "docker"];
|
|
82
65
|
var sandboxDriverCredentialFields = {
|
|
83
|
-
exe: [{ key: "api_token", label: "API Token", secret: true }],
|
|
84
|
-
daytona: [{ key: "api_key", label: "API Key", secret: true }],
|
|
85
66
|
modal: [
|
|
86
67
|
{ key: "token_id", label: "Token ID", secret: true },
|
|
87
68
|
{ key: "token_secret", label: "Token Secret", secret: true }
|
|
@@ -95,16 +76,14 @@ var sandboxDriverCredentialFields = {
|
|
|
95
76
|
{ key: "api_token", label: "API Token", secret: true },
|
|
96
77
|
{ key: "worker_url", label: "Worker URL" }
|
|
97
78
|
],
|
|
98
|
-
|
|
79
|
+
boat: [{ key: "api_key", label: "API Key", secret: true }],
|
|
99
80
|
docker: [{ key: "image", label: "Image" }]
|
|
100
81
|
};
|
|
101
82
|
var sandboxDriverLabels = {
|
|
102
|
-
exe: "exe.dev",
|
|
103
|
-
daytona: "Daytona",
|
|
104
83
|
modal: "Modal",
|
|
105
84
|
vercel: "Vercel",
|
|
106
85
|
cloudflare: "Cloudflare",
|
|
107
|
-
|
|
86
|
+
boat: "Boat",
|
|
108
87
|
docker: "Docker"
|
|
109
88
|
};
|
|
110
89
|
function isSandboxDriverID(input) {
|
|
@@ -117,14 +96,6 @@ function dockerSandboxDriverEnabled(env = process.env) {
|
|
|
117
96
|
return enabled(env.CLAXEDO_ENABLE_DOCKER_SANDBOX) || enabled(env.CLAXEDO_DEV_DOCKER_SANDBOX);
|
|
118
97
|
}
|
|
119
98
|
function sandboxDriverAuthValues(cfg, id, env = process.env) {
|
|
120
|
-
if (id === "daytona") {
|
|
121
|
-
const api_key = trimToUndefined(cfg?.auth?.daytona?.api_key);
|
|
122
|
-
return api_key ? { api_key } : void 0;
|
|
123
|
-
}
|
|
124
|
-
if (id === "exe") {
|
|
125
|
-
const api_token = trimToUndefined(cfg?.auth?.exe?.api_token) ?? trimToUndefined(env.EXE_DEV_API_TOKEN);
|
|
126
|
-
return api_token ? { api_token } : void 0;
|
|
127
|
-
}
|
|
128
99
|
if (id === "modal") {
|
|
129
100
|
const token_id = trimToUndefined(cfg?.auth?.modal?.token_id) ?? trimToUndefined(env.MODAL_TOKEN_ID);
|
|
130
101
|
const token_secret = trimToUndefined(cfg?.auth?.modal?.token_secret) ?? trimToUndefined(env.MODAL_TOKEN_SECRET);
|
|
@@ -138,93 +109,72 @@ function sandboxDriverAuthValues(cfg, id, env = process.env) {
|
|
|
138
109
|
}
|
|
139
110
|
if (id === "cloudflare") {
|
|
140
111
|
const api_token = trimToUndefined(cfg?.auth?.cloudflare?.api_token) ?? trimToUndefined(env.CLOUDFLARE_API_TOKEN);
|
|
141
|
-
const
|
|
112
|
+
const configuredUrl = trimToUndefined(cfg?.auth?.cloudflare?.worker_url) ?? trimToUndefined(env.CLOUDFLARE_SANDBOX_WORKER_URL);
|
|
113
|
+
const worker_url = configuredUrl ? cloudflareWorkerBaseUrl(configuredUrl) : void 0;
|
|
142
114
|
return api_token && worker_url ? { api_token, worker_url } : void 0;
|
|
143
115
|
}
|
|
144
|
-
if (id === "
|
|
145
|
-
const api_key = trimToUndefined(cfg?.auth?.
|
|
116
|
+
if (id === "boat") {
|
|
117
|
+
const api_key = trimToUndefined(cfg?.auth?.boat?.api_key) ?? trimToUndefined(env.BOAT_API_KEY);
|
|
146
118
|
return api_key ? { api_key } : void 0;
|
|
147
119
|
}
|
|
148
|
-
if (!dockerSandboxDriverEnabled(env))
|
|
120
|
+
if (!dockerSandboxDriverEnabled(env))
|
|
121
|
+
return;
|
|
149
122
|
const image = trimToUndefined(cfg?.auth?.docker?.image) ?? trimToUndefined(env.CLAXEDO_DOCKER_SANDBOX_IMAGE) ?? trimToUndefined(env.CLAXEDO_SANDBOX_IMAGE);
|
|
150
123
|
return image ? { image } : {};
|
|
151
124
|
}
|
|
125
|
+
function cloudflareWorkerBaseUrl(input) {
|
|
126
|
+
let url;
|
|
127
|
+
try {
|
|
128
|
+
url = new URL(input.trim());
|
|
129
|
+
} catch {
|
|
130
|
+
throw new Error("Cloudflare Worker URL must be a valid HTTPS endpoint");
|
|
131
|
+
}
|
|
132
|
+
if (url.protocol !== "https:" || url.username || url.password || input.includes("?") || input.includes("#")) {
|
|
133
|
+
throw new Error("Cloudflare Worker URL requires HTTPS without credentials, query or fragment");
|
|
134
|
+
}
|
|
135
|
+
return url.origin + url.pathname.replace(/\/+$/, "");
|
|
136
|
+
}
|
|
152
137
|
function sandboxDriverId(input, cfg, env = process.env) {
|
|
153
|
-
if (!isSandboxDriverID(input))
|
|
154
|
-
|
|
138
|
+
if (!isSandboxDriverID(input))
|
|
139
|
+
return;
|
|
140
|
+
if (input === "docker" && !sandboxDriverAuthValues(cfg, input, env))
|
|
141
|
+
return;
|
|
155
142
|
return input;
|
|
156
143
|
}
|
|
157
|
-
function defaultSandboxDriverID(cfg, env = process.env) {
|
|
158
|
-
return sandboxDriverId(cfg?.default_driver, cfg, env) ?? (enabled(env.CLAXEDO_DOCKER_SANDBOX_DEFAULT) && sandboxDriverAuthValues(cfg, "docker", env) ? "docker" : "daytona");
|
|
159
|
-
}
|
|
160
|
-
function listSandboxDrivers(cfg, env = process.env, managedDriverIds = /* @__PURE__ */ new Set()) {
|
|
161
|
-
const defaultDriver = defaultSandboxDriverID(cfg, env);
|
|
162
|
-
return {
|
|
163
|
-
default_driver: defaultDriver,
|
|
164
|
-
drivers: sandboxDriverIds.filter((id) => id !== "docker" || !!sandboxDriverAuthValues(cfg, "docker", env)).map((id) => {
|
|
165
|
-
const configuredFromConfigOrEnv = !!sandboxDriverAuthValues(cfg, id, env);
|
|
166
|
-
const configured = id === "docker" ? configuredFromConfigOrEnv : configuredFromConfigOrEnv || managedDriverIds.has(id);
|
|
167
|
-
return {
|
|
168
|
-
id,
|
|
169
|
-
label: sandboxDriverLabels[id],
|
|
170
|
-
fields: sandboxDriverCredentialFields[id],
|
|
171
|
-
configured,
|
|
172
|
-
source: configuredFromConfigOrEnv ? "config" : configured ? "managed" : "none",
|
|
173
|
-
default: id === defaultDriver
|
|
174
|
-
};
|
|
175
|
-
})
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
144
|
|
|
179
145
|
// src/driver-catalog.ts
|
|
180
|
-
var
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
},
|
|
202
|
-
daytona: {
|
|
203
|
-
id: "daytona",
|
|
204
|
-
label: sandboxDriverLabels.daytona,
|
|
205
|
-
credentialFields: sandboxDriverCredentialFields.daytona,
|
|
206
|
-
metadata: {
|
|
207
|
-
driverRunsIn: ["worker", "node"],
|
|
208
|
-
hostStopBehavior: "suspends-host",
|
|
209
|
-
hostResumeBehavior: "same-host",
|
|
210
|
-
targetAccess: "relay",
|
|
211
|
-
secretBrokering: "native",
|
|
212
|
-
// The driver translates names to domainAllowList, or addresses to networkAllowList.
|
|
213
|
-
egressControl: "hosts-and-cidrs",
|
|
214
|
-
persistence: {
|
|
215
|
-
resume: "same-sandbox",
|
|
216
|
-
capture: "filesystem",
|
|
217
|
-
clone: false,
|
|
218
|
-
captureSource: "preserved",
|
|
219
|
-
retention: "provider-managed",
|
|
220
|
-
restoreMount: "new-resource"
|
|
221
|
-
}
|
|
146
|
+
var localBrokeringTestDriverCatalogEntry = {
|
|
147
|
+
id: "local-brokering-test",
|
|
148
|
+
label: "Local brokering test driver",
|
|
149
|
+
credentialFields: [],
|
|
150
|
+
runtimeNetwork: { controlPlane: "direct", relay: "local" },
|
|
151
|
+
runtimeEnv: "ensure",
|
|
152
|
+
preparedImage: false,
|
|
153
|
+
metadata: {
|
|
154
|
+
driverRunsIn: ["local"],
|
|
155
|
+
hostStopBehavior: "terminates-host",
|
|
156
|
+
hostResumeBehavior: "same-host",
|
|
157
|
+
targetAccess: "loopback",
|
|
158
|
+
secretBrokering: "native",
|
|
159
|
+
egressControl: "hosts",
|
|
160
|
+
persistence: {
|
|
161
|
+
resume: "same-sandbox",
|
|
162
|
+
capture: "none",
|
|
163
|
+
clone: false,
|
|
164
|
+
captureSource: "not-applicable",
|
|
165
|
+
retention: "provider-managed",
|
|
166
|
+
restoreMount: "same-resource"
|
|
222
167
|
}
|
|
223
|
-
}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
var sandboxDriverCatalog = {
|
|
224
171
|
modal: {
|
|
225
172
|
id: "modal",
|
|
226
173
|
label: sandboxDriverLabels.modal,
|
|
227
174
|
credentialFields: sandboxDriverCredentialFields.modal,
|
|
175
|
+
runtimeNetwork: { controlPlane: "direct", relay: "configured" },
|
|
176
|
+
runtimeEnv: "callback",
|
|
177
|
+
preparedImage: true,
|
|
228
178
|
metadata: {
|
|
229
179
|
driverRunsIn: ["node"],
|
|
230
180
|
hostStopBehavior: "terminates-host",
|
|
@@ -247,6 +197,9 @@ var sandboxDriverCatalog = {
|
|
|
247
197
|
id: "vercel",
|
|
248
198
|
label: sandboxDriverLabels.vercel,
|
|
249
199
|
credentialFields: sandboxDriverCredentialFields.vercel,
|
|
200
|
+
runtimeNetwork: { controlPlane: "direct", relay: "configured" },
|
|
201
|
+
runtimeEnv: "callback",
|
|
202
|
+
preparedImage: false,
|
|
250
203
|
metadata: {
|
|
251
204
|
driverRunsIn: ["node"],
|
|
252
205
|
hostStopBehavior: "terminates-host",
|
|
@@ -269,30 +222,40 @@ var sandboxDriverCatalog = {
|
|
|
269
222
|
id: "cloudflare",
|
|
270
223
|
label: sandboxDriverLabels.cloudflare,
|
|
271
224
|
credentialFields: sandboxDriverCredentialFields.cloudflare,
|
|
225
|
+
runtimeNetwork: { controlPlane: "direct", relay: "configured" },
|
|
226
|
+
runtimeEnv: "callback",
|
|
227
|
+
preparedImage: false,
|
|
228
|
+
billing: cloudflareBilling,
|
|
272
229
|
metadata: {
|
|
273
230
|
driverRunsIn: ["worker"],
|
|
274
|
-
hostStopBehavior: "
|
|
231
|
+
hostStopBehavior: "terminates-host",
|
|
275
232
|
hostResumeBehavior: "same-host",
|
|
233
|
+
hostWatchesIdle: true,
|
|
276
234
|
targetAccess: "relay",
|
|
277
235
|
secretBrokering: "native",
|
|
278
236
|
// Credential handlers do not restrict unrelated destinations.
|
|
279
237
|
egressControl: "none",
|
|
238
|
+
// A container snapshot holds the whole root filesystem; the platform keeps it 30 days past its last restore and offers no deletion.
|
|
280
239
|
persistence: {
|
|
281
240
|
resume: "replacement-restore",
|
|
282
|
-
capture: "
|
|
241
|
+
capture: "filesystem",
|
|
283
242
|
clone: false,
|
|
284
243
|
captureSource: "preserved",
|
|
285
244
|
retention: "provider-managed",
|
|
286
|
-
restoreMount: "
|
|
245
|
+
restoreMount: "new-resource"
|
|
287
246
|
}
|
|
288
247
|
}
|
|
289
248
|
},
|
|
290
|
-
|
|
291
|
-
id: "
|
|
292
|
-
label: sandboxDriverLabels.
|
|
293
|
-
credentialFields: sandboxDriverCredentialFields.
|
|
249
|
+
boat: {
|
|
250
|
+
id: "boat",
|
|
251
|
+
label: sandboxDriverLabels.boat,
|
|
252
|
+
credentialFields: sandboxDriverCredentialFields.boat,
|
|
253
|
+
runtimeNetwork: { controlPlane: "direct", relay: "configured" },
|
|
254
|
+
runtimeEnv: "callback",
|
|
255
|
+
preparedImage: true,
|
|
256
|
+
billing: boatBilling,
|
|
294
257
|
metadata: {
|
|
295
|
-
driverRunsIn: ["node"],
|
|
258
|
+
driverRunsIn: ["worker", "node"],
|
|
296
259
|
hostStopBehavior: "suspends-host",
|
|
297
260
|
hostResumeBehavior: "same-host",
|
|
298
261
|
targetAccess: "relay",
|
|
@@ -305,13 +268,17 @@ var sandboxDriverCatalog = {
|
|
|
305
268
|
captureSource: "not-applicable",
|
|
306
269
|
retention: "provider-managed",
|
|
307
270
|
restoreMount: "same-resource"
|
|
308
|
-
}
|
|
271
|
+
},
|
|
272
|
+
machineClasses: ["small", "default", "large"]
|
|
309
273
|
}
|
|
310
274
|
},
|
|
311
275
|
docker: {
|
|
312
276
|
id: "docker",
|
|
313
277
|
label: sandboxDriverLabels.docker,
|
|
314
278
|
credentialFields: sandboxDriverCredentialFields.docker,
|
|
279
|
+
runtimeNetwork: { controlPlane: "docker-host", relay: "configured" },
|
|
280
|
+
runtimeEnv: "callback",
|
|
281
|
+
preparedImage: true,
|
|
315
282
|
metadata: {
|
|
316
283
|
driverRunsIn: ["local"],
|
|
317
284
|
hostStopBehavior: "terminates-host",
|
|
@@ -330,28 +297,15 @@ var sandboxDriverCatalog = {
|
|
|
330
297
|
}
|
|
331
298
|
}
|
|
332
299
|
};
|
|
333
|
-
function sandboxDriverAuth(cfg, id, env = process.env) {
|
|
334
|
-
if (id === "docker") {
|
|
335
|
-
const configured = sandboxDriverAuthValues(cfg, "docker", env);
|
|
336
|
-
if (!configured) return void 0;
|
|
337
|
-
const image = configured.image ?? defaultSandboxImage(workspaceRuntimeVersion(), void 0, env);
|
|
338
|
-
return { image };
|
|
339
|
-
}
|
|
340
|
-
return sandboxDriverAuthValues(cfg, id, env);
|
|
341
|
-
}
|
|
342
|
-
function hasSandboxDriverAuth(cfg, id, env = process.env) {
|
|
343
|
-
return !!sandboxDriverAuth(cfg, id, env);
|
|
344
|
-
}
|
|
345
300
|
function dockerSandboxDriverEnabled2(env = process.env) {
|
|
346
301
|
return dockerSandboxDriverEnabled(env);
|
|
347
302
|
}
|
|
348
303
|
export {
|
|
349
|
-
defaultSandboxDriverID,
|
|
350
304
|
dockerSandboxDriverEnabled2 as dockerSandboxDriverEnabled,
|
|
351
|
-
|
|
352
|
-
listSandboxDrivers,
|
|
353
|
-
sandboxDriverAuth,
|
|
305
|
+
localBrokeringTestDriverCatalogEntry,
|
|
354
306
|
sandboxDriverCatalog,
|
|
355
307
|
sandboxDriverId,
|
|
308
|
+
sandboxMachineCostUsd,
|
|
309
|
+
sandboxMachineRate,
|
|
356
310
|
validateSandboxPersistenceCapabilities
|
|
357
311
|
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { SandboxMachineClass } from "@claxedo/sandbox-contract";
|
|
2
|
+
import { type SandboxStartLimit, type SandboxStartWindow } from "../lease-failure";
|
|
3
|
+
export type BoatSandboxState = "init" | "provisioning" | "provisioned" | "cloning" | "ready" | "idle" | "running" | "archiving" | "archived" | "error" | "cancelled";
|
|
4
|
+
/** `hydrated`: Boat has downloaded every file of the snapshot the sandbox started from (docs.boat.dev/readiness). */
|
|
5
|
+
export type BoatSandbox = {
|
|
6
|
+
id: string;
|
|
7
|
+
state: BoatSandboxState;
|
|
8
|
+
hydrated: boolean;
|
|
9
|
+
};
|
|
10
|
+
export type BoatCommandResult = {
|
|
11
|
+
success: boolean;
|
|
12
|
+
stdout: string;
|
|
13
|
+
stderr: string;
|
|
14
|
+
exitCode: number | null;
|
|
15
|
+
timedOut: boolean;
|
|
16
|
+
};
|
|
17
|
+
export type BoatFetch = (input: string, init?: RequestInit) => Promise<Response>;
|
|
18
|
+
/**
|
|
19
|
+
* What Boat's 429 `rate_limited` says about the account's start limit: the
|
|
20
|
+
* window that is used up, its limit, and the starts used of each window. It
|
|
21
|
+
* names no time the window reopens (2026-10-15), so none is kept.
|
|
22
|
+
*/
|
|
23
|
+
export type BoatStartLimit = {
|
|
24
|
+
window: SandboxStartWindow;
|
|
25
|
+
limit: number;
|
|
26
|
+
starts: Partial<Record<SandboxStartWindow, {
|
|
27
|
+
limit: number;
|
|
28
|
+
used: number;
|
|
29
|
+
}>>;
|
|
30
|
+
};
|
|
31
|
+
export declare class BoatApiError extends Error {
|
|
32
|
+
readonly code: string;
|
|
33
|
+
readonly status?: number | undefined;
|
|
34
|
+
/** Boat answers 401 or 403 to the API key itself, never to a sandbox it was asked about. */
|
|
35
|
+
readonly providerKeyRefused: boolean;
|
|
36
|
+
/** Create, fork and resume each count as one start (Boat's own refusal, 2026-10-15). */
|
|
37
|
+
readonly providerRateLimited?: SandboxStartLimit;
|
|
38
|
+
readonly startLimit?: BoatStartLimit;
|
|
39
|
+
/** The state Boat's refusal gives the sandbox it refused, e.g. `archived` on a 400 `stop_failed` (2026-10-17). */
|
|
40
|
+
readonly sandboxState?: BoatSandboxState;
|
|
41
|
+
constructor(message: string, code: string, status?: number | undefined, refusal?: {
|
|
42
|
+
startLimit?: BoatStartLimit;
|
|
43
|
+
sandboxState?: BoatSandboxState;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export declare function invalid(): never;
|
|
47
|
+
export declare function requiredText(value: unknown): string;
|
|
48
|
+
export type BoatClientOptions = {
|
|
49
|
+
apiKey: string;
|
|
50
|
+
baseUrl?: string;
|
|
51
|
+
timeoutMs?: number;
|
|
52
|
+
fetchImpl?: BoatFetch;
|
|
53
|
+
};
|
|
54
|
+
export type BoatApi = ReturnType<typeof createBoatApi>;
|
|
55
|
+
/** One Boat v1 call: the bearer key, the deadline, the bounded envelope, and the envelope types the call accepts. */
|
|
56
|
+
export declare function createBoatApi(options: BoatClientOptions): (path: string, types: readonly string[], init?: {
|
|
57
|
+
method?: string;
|
|
58
|
+
body?: unknown;
|
|
59
|
+
headers?: Record<string, string>;
|
|
60
|
+
timeoutMs?: number;
|
|
61
|
+
}) => Promise<Record<string, unknown>>;
|
|
62
|
+
export declare function createBoatClient(options: BoatClientOptions): {
|
|
63
|
+
/** `from` is a named snapshot or a share code (`bsh_…`) the sandbox starts from; omitted, Boat's base machine. */
|
|
64
|
+
create(input: {
|
|
65
|
+
idempotencyKey: string;
|
|
66
|
+
ttlSeconds: number | null;
|
|
67
|
+
type?: SandboxMachineClass;
|
|
68
|
+
from?: string;
|
|
69
|
+
}): Promise<BoatSandbox>;
|
|
70
|
+
get(id: string): Promise<BoatSandbox>;
|
|
71
|
+
/** `type` resumes the sandbox onto that machine size; omitted, it keeps the size it has (docs.boat.dev/machines/change-size). */
|
|
72
|
+
resume(id: string, input: {
|
|
73
|
+
ttlSeconds: number | null;
|
|
74
|
+
type?: SandboxMachineClass;
|
|
75
|
+
}): Promise<void>;
|
|
76
|
+
/**
|
|
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` (2026-10-13).
|
|
79
|
+
*/
|
|
80
|
+
update(id: string, input: {
|
|
81
|
+
ttlSeconds: number | null;
|
|
82
|
+
}): Promise<BoatSandbox>;
|
|
83
|
+
stop(id: string): Promise<void>;
|
|
84
|
+
delete(id: string): Promise<void>;
|
|
85
|
+
writeFile(id: string, input: {
|
|
86
|
+
path: string;
|
|
87
|
+
content: string;
|
|
88
|
+
}): Promise<void>;
|
|
89
|
+
/** `ready` once a command runs, which Boat answers the moment the sandbox is ready; `starting` when Boat's hold ended first. */
|
|
90
|
+
whenReady(id: string): Promise<"ready" | "starting">;
|
|
91
|
+
command(id: string, input: {
|
|
92
|
+
command: string;
|
|
93
|
+
timeoutSeconds?: number;
|
|
94
|
+
}): Promise<BoatCommandResult>;
|
|
95
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type BoatClientOptions } from "./boat-client";
|
|
2
|
+
export type BoatNamedSnapshotStatus = "saving" | "ready" | "failed";
|
|
3
|
+
export type BoatNamedSnapshot = {
|
|
4
|
+
name: string;
|
|
5
|
+
status: BoatNamedSnapshotStatus;
|
|
6
|
+
createdAt: string;
|
|
7
|
+
};
|
|
8
|
+
export type BoatSnapshotShare = {
|
|
9
|
+
id: string;
|
|
10
|
+
status: "active" | "revoked";
|
|
11
|
+
createdAt: string;
|
|
12
|
+
};
|
|
13
|
+
/** Boat's named snapshots and their share codes (docs.boat.dev/snapshots/templates). */
|
|
14
|
+
export declare function createBoatNamedSnapshotClient(options: BoatClientOptions): {
|
|
15
|
+
/** A stopped sandbox is saved from its last snapshot; the save settles in the background. */
|
|
16
|
+
save(input: {
|
|
17
|
+
sandboxId: string;
|
|
18
|
+
name: string;
|
|
19
|
+
}): Promise<BoatNamedSnapshot>;
|
|
20
|
+
get(name: string): Promise<BoatNamedSnapshot | undefined>;
|
|
21
|
+
/** Newest first. */
|
|
22
|
+
list(): Promise<BoatNamedSnapshot[]>;
|
|
23
|
+
delete(name: string): Promise<void>;
|
|
24
|
+
/** Boat shows the code only in this answer and keeps a hash of it. */
|
|
25
|
+
share(name: string): Promise<{
|
|
26
|
+
shareCode: string;
|
|
27
|
+
share: BoatSnapshotShare;
|
|
28
|
+
}>;
|
|
29
|
+
shares(name: string): Promise<BoatSnapshotShare[]>;
|
|
30
|
+
revokeShare(name: string, shareId: string): Promise<void>;
|
|
31
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type BoatRuntimeTemplate, type BoatSandboxDriverOptions } from "./boat";
|
|
2
|
+
import { type BoatClientOptions } from "./boat-client";
|
|
3
|
+
export type BoatRuntimeTemplateOptions = BoatClientOptions & {
|
|
4
|
+
image: string;
|
|
5
|
+
/** Template names start with it, and pruning removes only names that do. */
|
|
6
|
+
prefix?: string;
|
|
7
|
+
/** How many templates of the prefix pruning keeps, the image's own and the newest others. */
|
|
8
|
+
keep?: number;
|
|
9
|
+
log?: (line: string) => void;
|
|
10
|
+
pollIntervalMs?: number;
|
|
11
|
+
hydrationTimeoutMs?: number;
|
|
12
|
+
stopTimeoutMs?: number;
|
|
13
|
+
saveTimeoutMs?: number;
|
|
14
|
+
driver?: Pick<BoatSandboxDriverOptions, "statePollMs" | "provisionTimeoutMs" | "healthIntervalMs" | "healthTimeoutMs">;
|
|
15
|
+
};
|
|
16
|
+
/** The image reference is content-addressed (its tag names the build), and the seeded trees are named by it, so it keys the template. */
|
|
17
|
+
export declare function boatRuntimeTemplateName(image: string, prefix?: string): Promise<string>;
|
|
18
|
+
/**
|
|
19
|
+
* Saves the template once per image: a ready template is reused, one still
|
|
20
|
+
* saving is waited for, and a missing or failed one is built on a builder
|
|
21
|
+
* sandbox that is deleted whatever the outcome.
|
|
22
|
+
*/
|
|
23
|
+
export declare function ensureBoatRuntimeTemplate(options: BoatRuntimeTemplateOptions): Promise<{
|
|
24
|
+
name: string;
|
|
25
|
+
built: boolean;
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* A new share code for the image's template, revoking none: until the deploy
|
|
29
|
+
* that carries it has succeeded, the running Worker creates with the code it
|
|
30
|
+
* holds. `shareId` names the code without being the secret.
|
|
31
|
+
*/
|
|
32
|
+
export declare function shareBoatRuntimeTemplate(options: BoatRuntimeTemplateOptions): Promise<{
|
|
33
|
+
template: BoatRuntimeTemplate;
|
|
34
|
+
shareId: string;
|
|
35
|
+
}>;
|
|
36
|
+
/**
|
|
37
|
+
* Run once the deploy carrying `deployedShareId` succeeded: every other code of
|
|
38
|
+
* the deployed image's template is revoked, those of earlier deploys and of
|
|
39
|
+
* deploys that failed alike.
|
|
40
|
+
*/
|
|
41
|
+
export declare function revokeBoatRuntimeTemplateShares(options: BoatRuntimeTemplateOptions, deployedShareId: string): Promise<string[]>;
|
|
42
|
+
/**
|
|
43
|
+
* Run once the deploy of `options.image` succeeded, so the template it keeps
|
|
44
|
+
* is the one the live Worker creates from. Each template past Boat's ten free
|
|
45
|
+
* named snapshots costs $1.70 a month, and a template serves only the image it
|
|
46
|
+
* was built for, so templates of the prefix beyond the newest `keep` (the
|
|
47
|
+
* deployed image's among them) are removed; Boat removes their codes with them.
|
|
48
|
+
*/
|
|
49
|
+
export declare function pruneBoatRuntimeTemplates(options: BoatRuntimeTemplateOptions): Promise<string[]>;
|