@claxedo/sandbox-manager 0.9.0 → 0.10.0
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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type SandboxDriver, type SandboxDriverEnsureInput, type SandboxListingUnsupported } from "
|
|
1
|
+
import { type SandboxDriver, type SandboxDriverEnsureInput, type SandboxListingUnsupported } from "../contract";
|
|
2
2
|
import { type WorkspaceRuntimeControlEnv } from "../runtime-env";
|
|
3
3
|
export type CloudflareSandboxDriverOptions = {
|
|
4
4
|
/** Base URL of the deployed Cloudflare Sandbox Worker (e.g. https://sbx.example.com). */
|
|
@@ -20,8 +20,8 @@ export type CloudflareSandboxDriverOptions = {
|
|
|
20
20
|
* managementJwksUrl → WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL (config apply)
|
|
21
21
|
*/
|
|
22
22
|
controlEnv?: WorkspaceRuntimeControlEnv;
|
|
23
|
-
/** Default
|
|
24
|
-
|
|
23
|
+
/** Default native harness injected as WORKSPACE_RUNTIME_NATIVE_HARNESS, for example "opencode". */
|
|
24
|
+
nativeHarness?: string;
|
|
25
25
|
/** Dynamic runtime env that needs the sandbox id or current lease. */
|
|
26
26
|
env?: (input: SandboxDriverEnsureInput, sandbox: {
|
|
27
27
|
id: string;
|
|
@@ -1,44 +1,41 @@
|
|
|
1
1
|
// src/constants.ts
|
|
2
2
|
var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
|
|
3
3
|
|
|
4
|
-
// src/
|
|
5
|
-
var
|
|
6
|
-
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return `claxedo-broker:${name}`;
|
|
33
|
-
}
|
|
34
|
-
function brokeredPlaceholderEnv(secrets) {
|
|
35
|
-
return Object.fromEntries((secrets ?? []).map((secret) => [secret.name, brokeredSecretPlaceholder(secret.name)]));
|
|
36
|
-
}
|
|
4
|
+
// src/driver-billing.ts
|
|
5
|
+
var HOUR_SECONDS = 3600;
|
|
6
|
+
var boatBilling = {
|
|
7
|
+
source: { url: "https://docs.boat.dev/pricing", readOn: "2026-10-12" },
|
|
8
|
+
meter: "Boat bills machine time per second while a sandbox runs, at an hourly rate per size; a stopped sandbox costs nothing.",
|
|
9
|
+
rates: {
|
|
10
|
+
small: { size: "small", vcpu: 2, memoryGb: 4, usdPerHour: 0.018 },
|
|
11
|
+
default: { size: "default", vcpu: 4, memoryGb: 8, usdPerHour: 0.036 },
|
|
12
|
+
large: { size: "large", vcpu: 8, memoryGb: 16, usdPerHour: 0.072 }
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var CLOUDFLARE_USD_PER_GIB_SECOND_MEMORY = 25e-7;
|
|
16
|
+
var CLOUDFLARE_USD_PER_GB_SECOND_DISK = 7e-8;
|
|
17
|
+
var CLOUDFLARE_USD_PER_VCPU_SECOND_ACTIVE = 2e-5;
|
|
18
|
+
var CLOUDFLARE_STANDARD_1 = { size: "standard-1", vcpu: 0.5, memoryGb: 4, diskGb: 8 };
|
|
19
|
+
var cloudflareBilling = {
|
|
20
|
+
source: { url: "https://developers.cloudflare.com/containers/pricing/", readOn: "2026-10-12" },
|
|
21
|
+
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.",
|
|
22
|
+
rates: {
|
|
23
|
+
default: {
|
|
24
|
+
size: CLOUDFLARE_STANDARD_1.size,
|
|
25
|
+
vcpu: CLOUDFLARE_STANDARD_1.vcpu,
|
|
26
|
+
memoryGb: CLOUDFLARE_STANDARD_1.memoryGb,
|
|
27
|
+
usdPerHour: (CLOUDFLARE_STANDARD_1.memoryGb * CLOUDFLARE_USD_PER_GIB_SECOND_MEMORY + CLOUDFLARE_STANDARD_1.diskGb * CLOUDFLARE_USD_PER_GB_SECOND_DISK) * HOUR_SECONDS
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
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`
|
|
31
|
+
};
|
|
37
32
|
|
|
38
33
|
// ../sandbox-contract/dist/index.mjs
|
|
34
|
+
var sandboxCheckpointUnusableCauses = ["expired", "runtime_outdated", "runtime_modified", "not_restorable"];
|
|
35
|
+
function isSandboxCheckpointUnusableCause(value) {
|
|
36
|
+
return sandboxCheckpointUnusableCauses.some((cause) => cause === value);
|
|
37
|
+
}
|
|
39
38
|
var sandboxDriverCredentialFields = {
|
|
40
|
-
exe: [{ key: "api_token", label: "API Token", secret: true }],
|
|
41
|
-
daytona: [{ key: "api_key", label: "API Key", secret: true }],
|
|
42
39
|
modal: [
|
|
43
40
|
{ key: "token_id", label: "Token ID", secret: true },
|
|
44
41
|
{ key: "token_secret", label: "Token Secret", secret: true }
|
|
@@ -52,68 +49,38 @@ var sandboxDriverCredentialFields = {
|
|
|
52
49
|
{ key: "api_token", label: "API Token", secret: true },
|
|
53
50
|
{ key: "worker_url", label: "Worker URL" }
|
|
54
51
|
],
|
|
55
|
-
|
|
52
|
+
boat: [{ key: "api_key", label: "API Key", secret: true }],
|
|
56
53
|
docker: [{ key: "image", label: "Image" }]
|
|
57
54
|
};
|
|
58
55
|
var sandboxDriverLabels = {
|
|
59
|
-
exe: "exe.dev",
|
|
60
|
-
daytona: "Daytona",
|
|
61
56
|
modal: "Modal",
|
|
62
57
|
vercel: "Vercel",
|
|
63
58
|
cloudflare: "Cloudflare",
|
|
64
|
-
|
|
59
|
+
boat: "Boat",
|
|
65
60
|
docker: "Docker"
|
|
66
61
|
};
|
|
62
|
+
function cloudflareWorkerBaseUrl(input) {
|
|
63
|
+
let url;
|
|
64
|
+
try {
|
|
65
|
+
url = new URL(input.trim());
|
|
66
|
+
} catch {
|
|
67
|
+
throw new Error("Cloudflare Worker URL must be a valid HTTPS endpoint");
|
|
68
|
+
}
|
|
69
|
+
if (url.protocol !== "https:" || url.username || url.password || input.includes("?") || input.includes("#")) {
|
|
70
|
+
throw new Error("Cloudflare Worker URL requires HTTPS without credentials, query or fragment");
|
|
71
|
+
}
|
|
72
|
+
return url.origin + url.pathname.replace(/\/+$/, "");
|
|
73
|
+
}
|
|
67
74
|
|
|
68
75
|
// src/driver-catalog.ts
|
|
69
76
|
var sandboxDriverCatalog = {
|
|
70
|
-
exe: {
|
|
71
|
-
id: "exe",
|
|
72
|
-
label: sandboxDriverLabels.exe,
|
|
73
|
-
credentialFields: sandboxDriverCredentialFields.exe,
|
|
74
|
-
metadata: {
|
|
75
|
-
driverRunsIn: ["worker", "node"],
|
|
76
|
-
hostStopBehavior: "not-supported",
|
|
77
|
-
hostResumeBehavior: "same-host",
|
|
78
|
-
targetAccess: "relay",
|
|
79
|
-
secretBrokering: "none",
|
|
80
|
-
egressControl: "none",
|
|
81
|
-
persistence: {
|
|
82
|
-
resume: "same-sandbox",
|
|
83
|
-
capture: "none",
|
|
84
|
-
clone: true,
|
|
85
|
-
captureSource: "not-applicable",
|
|
86
|
-
retention: "provider-managed",
|
|
87
|
-
restoreMount: "same-resource"
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
|
-
daytona: {
|
|
92
|
-
id: "daytona",
|
|
93
|
-
label: sandboxDriverLabels.daytona,
|
|
94
|
-
credentialFields: sandboxDriverCredentialFields.daytona,
|
|
95
|
-
metadata: {
|
|
96
|
-
driverRunsIn: ["worker", "node"],
|
|
97
|
-
hostStopBehavior: "suspends-host",
|
|
98
|
-
hostResumeBehavior: "same-host",
|
|
99
|
-
targetAccess: "relay",
|
|
100
|
-
secretBrokering: "native",
|
|
101
|
-
// The driver translates names to domainAllowList, or addresses to networkAllowList.
|
|
102
|
-
egressControl: "hosts-and-cidrs",
|
|
103
|
-
persistence: {
|
|
104
|
-
resume: "same-sandbox",
|
|
105
|
-
capture: "filesystem",
|
|
106
|
-
clone: false,
|
|
107
|
-
captureSource: "preserved",
|
|
108
|
-
retention: "provider-managed",
|
|
109
|
-
restoreMount: "new-resource"
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
77
|
modal: {
|
|
114
78
|
id: "modal",
|
|
115
79
|
label: sandboxDriverLabels.modal,
|
|
116
80
|
credentialFields: sandboxDriverCredentialFields.modal,
|
|
81
|
+
runtimeNetwork: { controlPlane: "direct", relay: "configured" },
|
|
82
|
+
runtimeEnv: "callback",
|
|
83
|
+
preparedImage: true,
|
|
117
84
|
metadata: {
|
|
118
85
|
driverRunsIn: ["node"],
|
|
119
86
|
hostStopBehavior: "terminates-host",
|
|
@@ -136,6 +103,9 @@ var sandboxDriverCatalog = {
|
|
|
136
103
|
id: "vercel",
|
|
137
104
|
label: sandboxDriverLabels.vercel,
|
|
138
105
|
credentialFields: sandboxDriverCredentialFields.vercel,
|
|
106
|
+
runtimeNetwork: { controlPlane: "direct", relay: "configured" },
|
|
107
|
+
runtimeEnv: "callback",
|
|
108
|
+
preparedImage: false,
|
|
139
109
|
metadata: {
|
|
140
110
|
driverRunsIn: ["node"],
|
|
141
111
|
hostStopBehavior: "terminates-host",
|
|
@@ -158,30 +128,40 @@ var sandboxDriverCatalog = {
|
|
|
158
128
|
id: "cloudflare",
|
|
159
129
|
label: sandboxDriverLabels.cloudflare,
|
|
160
130
|
credentialFields: sandboxDriverCredentialFields.cloudflare,
|
|
131
|
+
runtimeNetwork: { controlPlane: "direct", relay: "configured" },
|
|
132
|
+
runtimeEnv: "callback",
|
|
133
|
+
preparedImage: false,
|
|
134
|
+
billing: cloudflareBilling,
|
|
161
135
|
metadata: {
|
|
162
136
|
driverRunsIn: ["worker"],
|
|
163
|
-
hostStopBehavior: "
|
|
137
|
+
hostStopBehavior: "terminates-host",
|
|
164
138
|
hostResumeBehavior: "same-host",
|
|
139
|
+
hostWatchesIdle: true,
|
|
165
140
|
targetAccess: "relay",
|
|
166
141
|
secretBrokering: "native",
|
|
167
142
|
// Credential handlers do not restrict unrelated destinations.
|
|
168
143
|
egressControl: "none",
|
|
144
|
+
// A container snapshot holds the whole root filesystem; the platform keeps it 30 days past its last restore and offers no deletion.
|
|
169
145
|
persistence: {
|
|
170
146
|
resume: "replacement-restore",
|
|
171
|
-
capture: "
|
|
147
|
+
capture: "filesystem",
|
|
172
148
|
clone: false,
|
|
173
149
|
captureSource: "preserved",
|
|
174
150
|
retention: "provider-managed",
|
|
175
|
-
restoreMount: "
|
|
151
|
+
restoreMount: "new-resource"
|
|
176
152
|
}
|
|
177
153
|
}
|
|
178
154
|
},
|
|
179
|
-
|
|
180
|
-
id: "
|
|
181
|
-
label: sandboxDriverLabels.
|
|
182
|
-
credentialFields: sandboxDriverCredentialFields.
|
|
155
|
+
boat: {
|
|
156
|
+
id: "boat",
|
|
157
|
+
label: sandboxDriverLabels.boat,
|
|
158
|
+
credentialFields: sandboxDriverCredentialFields.boat,
|
|
159
|
+
runtimeNetwork: { controlPlane: "direct", relay: "configured" },
|
|
160
|
+
runtimeEnv: "callback",
|
|
161
|
+
preparedImage: true,
|
|
162
|
+
billing: boatBilling,
|
|
183
163
|
metadata: {
|
|
184
|
-
driverRunsIn: ["node"],
|
|
164
|
+
driverRunsIn: ["worker", "node"],
|
|
185
165
|
hostStopBehavior: "suspends-host",
|
|
186
166
|
hostResumeBehavior: "same-host",
|
|
187
167
|
targetAccess: "relay",
|
|
@@ -194,13 +174,17 @@ var sandboxDriverCatalog = {
|
|
|
194
174
|
captureSource: "not-applicable",
|
|
195
175
|
retention: "provider-managed",
|
|
196
176
|
restoreMount: "same-resource"
|
|
197
|
-
}
|
|
177
|
+
},
|
|
178
|
+
machineClasses: ["small", "default", "large"]
|
|
198
179
|
}
|
|
199
180
|
},
|
|
200
181
|
docker: {
|
|
201
182
|
id: "docker",
|
|
202
183
|
label: sandboxDriverLabels.docker,
|
|
203
184
|
credentialFields: sandboxDriverCredentialFields.docker,
|
|
185
|
+
runtimeNetwork: { controlPlane: "docker-host", relay: "configured" },
|
|
186
|
+
runtimeEnv: "callback",
|
|
187
|
+
preparedImage: true,
|
|
204
188
|
metadata: {
|
|
205
189
|
driverRunsIn: ["local"],
|
|
206
190
|
hostStopBehavior: "terminates-host",
|
|
@@ -220,6 +204,37 @@ var sandboxDriverCatalog = {
|
|
|
220
204
|
}
|
|
221
205
|
};
|
|
222
206
|
|
|
207
|
+
// src/egress-policy.ts
|
|
208
|
+
function brokeredSecretPlaceholder(name) {
|
|
209
|
+
return `claxedo-broker:${name}`;
|
|
210
|
+
}
|
|
211
|
+
function brokeredPlaceholderEnv(secrets) {
|
|
212
|
+
return Object.fromEntries((secrets ?? []).map((secret) => [secret.name, brokeredSecretPlaceholder(secret.name)]));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/lease-failure.ts
|
|
216
|
+
var SandboxRuntimeBootError = class extends Error {
|
|
217
|
+
runtimeBootFailed = true;
|
|
218
|
+
constructor(reason) {
|
|
219
|
+
super(reason);
|
|
220
|
+
this.name = "SandboxRuntimeBootError";
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
var SandboxCheckpointUnusableError = class extends Error {
|
|
224
|
+
constructor(checkpointUnusable, reason) {
|
|
225
|
+
super(reason);
|
|
226
|
+
this.checkpointUnusable = checkpointUnusable;
|
|
227
|
+
this.name = "SandboxCheckpointUnusableError";
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
var SandboxProviderKeyRefusedError = class extends Error {
|
|
231
|
+
providerKeyRefused = true;
|
|
232
|
+
constructor(reason) {
|
|
233
|
+
super(reason);
|
|
234
|
+
this.name = "SandboxProviderKeyRefusedError";
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
223
238
|
// ../claxedo-helpers/dist/guards.mjs
|
|
224
239
|
function isRecord(value) {
|
|
225
240
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -234,13 +249,43 @@ function text(input) {
|
|
|
234
249
|
}
|
|
235
250
|
|
|
236
251
|
// src/runtime-env.ts
|
|
252
|
+
var WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS = [
|
|
253
|
+
"WORKSPACE_RUNTIME_WORKSPACE_ID",
|
|
254
|
+
"WORKSPACE_RUNTIME_HOST_ID",
|
|
255
|
+
"WORKSPACE_RUNTIME_RELAY_WORKSPACE_IDS"
|
|
256
|
+
];
|
|
257
|
+
function workspaceRuntimeIdentityEnvConflicts(env) {
|
|
258
|
+
if (!env) return [];
|
|
259
|
+
return WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS.filter((key) => env[key] !== void 0);
|
|
260
|
+
}
|
|
261
|
+
function assertWorkspaceRuntimeIdentityEnv(env) {
|
|
262
|
+
const conflicts = workspaceRuntimeIdentityEnvConflicts(env);
|
|
263
|
+
if (conflicts.length === 0) return;
|
|
264
|
+
throw new Error(
|
|
265
|
+
`sandbox env cannot set runtime identity ${conflicts.join(", ")}: the driver's placement owns the identity its target reports and the lease records`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
var WORKSPACE_RUNTIME_PUBLIC_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
269
|
+
...WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS,
|
|
270
|
+
"WORKSPACE_RUNTIME_DIRECTORY",
|
|
271
|
+
"WORKSPACE_RUNTIME_PORT",
|
|
272
|
+
"WORKSPACE_RUNTIME_HOST",
|
|
273
|
+
"WORKSPACE_RUNTIME_SOURCE_KIND",
|
|
274
|
+
"WORKSPACE_RUNTIME_GIT_REPO_URL",
|
|
275
|
+
"WORKSPACE_RUNTIME_GIT_BRANCH",
|
|
276
|
+
"WORKSPACE_RUNTIME_NATIVE_HARNESS",
|
|
277
|
+
"WORKSPACE_RUNTIME_RELAY_JWKS_URL",
|
|
278
|
+
"WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL",
|
|
279
|
+
"WORKSPACE_RUNTIME_SESSION_AUTHORITY_URL"
|
|
280
|
+
]);
|
|
237
281
|
function workspaceRuntimeBootEnv(input) {
|
|
282
|
+
assertWorkspaceRuntimeIdentityEnv(input.env);
|
|
238
283
|
const env = {
|
|
239
284
|
...workspaceRuntimeTargetEnv(input),
|
|
240
285
|
...workspaceRuntimeSourceEnv(input),
|
|
241
286
|
...input.env
|
|
242
287
|
};
|
|
243
|
-
if (input.
|
|
288
|
+
if (input.nativeHarness) env.WORKSPACE_RUNTIME_NATIVE_HARNESS = input.nativeHarness;
|
|
244
289
|
if (input.controlEnv?.relayJwksUrl) env.WORKSPACE_RUNTIME_RELAY_JWKS_URL = input.controlEnv.relayJwksUrl;
|
|
245
290
|
if (input.controlEnv?.relayVerifyPem) env.WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM = input.controlEnv.relayVerifyPem;
|
|
246
291
|
if (input.controlEnv?.managementJwksUrl) env.WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL = input.controlEnv.managementJwksUrl;
|
|
@@ -265,21 +310,29 @@ function workspaceRuntimeSourceEnv(input) {
|
|
|
265
310
|
};
|
|
266
311
|
}
|
|
267
312
|
|
|
313
|
+
// src/runtime-version.ts
|
|
314
|
+
var MINIMUM_RESTORABLE_WORKSPACE_RUNTIME_VERSION = "0.5.2";
|
|
315
|
+
|
|
268
316
|
// src/drivers/cloudflare.ts
|
|
269
317
|
function registryEntries(input) {
|
|
270
|
-
if (!Array.isArray(input))
|
|
271
|
-
return input.
|
|
318
|
+
if (!Array.isArray(input)) throw new Error("Invalid Cloudflare sandbox listing: expected sandboxes array");
|
|
319
|
+
return input.map((item) => {
|
|
272
320
|
const row = asRecord(item);
|
|
273
|
-
if (!row)
|
|
274
|
-
|
|
275
|
-
|
|
321
|
+
if (!row || typeof row.sandboxId !== "string" || !row.sandboxId) {
|
|
322
|
+
throw new Error("Invalid Cloudflare sandbox listing entry: expected sandboxId");
|
|
323
|
+
}
|
|
324
|
+
const entries = Object.entries(row).map(([key, value]) => {
|
|
325
|
+
if (typeof value !== "string") throw new Error(`Invalid Cloudflare sandbox listing label: ${key}`);
|
|
326
|
+
return [key, value];
|
|
327
|
+
});
|
|
328
|
+
return Object.fromEntries(entries);
|
|
276
329
|
});
|
|
277
330
|
}
|
|
278
331
|
var DEFAULT_WORKSPACE_DIR = "/workspace";
|
|
279
332
|
var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
|
|
280
333
|
var DEFAULT_TIMEOUT_MS = 45e3;
|
|
281
334
|
var DEFAULT_ENSURE_TIMEOUT_MS = 3e5;
|
|
282
|
-
var
|
|
335
|
+
var DEFAULT_SNAPSHOT_TIMEOUT_MS = 18e4;
|
|
283
336
|
function deadlineSignal(timeoutMs) {
|
|
284
337
|
const controller = new AbortController();
|
|
285
338
|
const timer = setTimeout(
|
|
@@ -293,13 +346,7 @@ function deadlineSignal(timeoutMs) {
|
|
|
293
346
|
}
|
|
294
347
|
};
|
|
295
348
|
}
|
|
296
|
-
var
|
|
297
|
-
function captureDirectories(workspaceDirectory) {
|
|
298
|
-
return [workspaceDirectory, RUNTIME_DATA_DIR];
|
|
299
|
-
}
|
|
300
|
-
function cleanUrl(input) {
|
|
301
|
-
return input.replace(/\/+$/, "");
|
|
302
|
-
}
|
|
349
|
+
var RUNTIME_HOME = "/home/claxedo";
|
|
303
350
|
function sandboxIdFor(workspaceId) {
|
|
304
351
|
return `claxedo-${workspaceId}`;
|
|
305
352
|
}
|
|
@@ -311,20 +358,26 @@ var CloudflareSandboxListingUnsupportedError = class extends Error {
|
|
|
311
358
|
}
|
|
312
359
|
};
|
|
313
360
|
function createCloudflareSandboxDriver(options) {
|
|
314
|
-
const
|
|
361
|
+
const fetchWorker = options.fetch ?? fetch;
|
|
362
|
+
const doFetch = async (url, init) => {
|
|
363
|
+
const response = await fetchWorker(url, { ...init, redirect: "manual" });
|
|
364
|
+
if (response.redirected || response.status >= 300 && response.status < 400) throw new Error("Cloudflare Worker redirects are not allowed");
|
|
365
|
+
return response;
|
|
366
|
+
};
|
|
315
367
|
const runtimePort = options.runtimePort ?? DEFAULT_WORKSPACE_RUNTIME_PORT;
|
|
316
368
|
const runtimeCommand = options.runtimeCommand ?? DEFAULT_RUNTIME_COMMAND;
|
|
317
369
|
const workspaceDir = options.workspaceDir ?? DEFAULT_WORKSPACE_DIR;
|
|
318
370
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
319
371
|
const ensureTimeoutMs = options.timeoutMs ?? DEFAULT_ENSURE_TIMEOUT_MS;
|
|
320
|
-
const
|
|
372
|
+
const snapshotTimeoutMs = options.timeoutMs ?? DEFAULT_SNAPSHOT_TIMEOUT_MS;
|
|
373
|
+
const base = cloudflareWorkerBaseUrl(options.workerUrl);
|
|
321
374
|
const headers = {
|
|
322
375
|
Authorization: `Bearer ${options.apiToken}`,
|
|
323
376
|
"Content-Type": "application/json"
|
|
324
377
|
};
|
|
325
378
|
async function call(sandboxId, action, body, method = "POST") {
|
|
326
379
|
const url = `${base}/sandbox/${encodeURIComponent(sandboxId)}${action ? `/${action}` : ""}`;
|
|
327
|
-
const deadline = deadlineSignal(action === "ensure-runtime" ? ensureTimeoutMs : timeoutMs);
|
|
380
|
+
const deadline = deadlineSignal(action === "ensure-runtime" ? ensureTimeoutMs : action === "snapshot" ? snapshotTimeoutMs : timeoutMs);
|
|
328
381
|
try {
|
|
329
382
|
const res = await doFetch(url, {
|
|
330
383
|
method,
|
|
@@ -350,15 +403,15 @@ function createCloudflareSandboxDriver(options) {
|
|
|
350
403
|
host: "0.0.0.0",
|
|
351
404
|
source: input.source,
|
|
352
405
|
env: {
|
|
353
|
-
|
|
354
|
-
CLAXEDO_DATA_DIR:
|
|
406
|
+
HOME: RUNTIME_HOME,
|
|
407
|
+
CLAXEDO_DATA_DIR: `${RUNTIME_HOME}/.claxedo`,
|
|
355
408
|
...input.env,
|
|
356
409
|
...await options.env?.(input, { id: hostId }),
|
|
357
410
|
// Last, so no caller-supplied variable of the same name can stand in
|
|
358
411
|
// for a placeholder the outbound handler matches on.
|
|
359
412
|
...brokeredPlaceholderEnv(input.secrets)
|
|
360
413
|
},
|
|
361
|
-
|
|
414
|
+
nativeHarness: options.nativeHarness,
|
|
362
415
|
controlEnv: options.controlEnv
|
|
363
416
|
});
|
|
364
417
|
}
|
|
@@ -399,21 +452,26 @@ function createCloudflareSandboxDriver(options) {
|
|
|
399
452
|
// what makes this driver's `list()` (and therefore GC) possible at all.
|
|
400
453
|
// Not container env — labels are control-plane metadata.
|
|
401
454
|
labels: input.labels,
|
|
402
|
-
...input.bootSource?.kind === "driver-snapshot" ? { restore: {
|
|
455
|
+
...input.bootSource?.kind === "driver-snapshot" ? { restore: { snapshotId: input.bootSource.snapshotId }, minimumRuntimeVersion: MINIMUM_RESTORABLE_WORKSPACE_RUNTIME_VERSION } : {},
|
|
403
456
|
...egress ? { egress } : {}
|
|
404
457
|
}
|
|
405
458
|
).catch((error) => {
|
|
406
459
|
if (error instanceof Error && error.name === "TimeoutError") return void 0;
|
|
407
460
|
throw error;
|
|
408
461
|
});
|
|
409
|
-
if (!response) return { provisioning: true, retryAfterMs:
|
|
462
|
+
if (!response) return { provisioning: true, retryAfterMs: 0 };
|
|
410
463
|
const { status, data } = response;
|
|
411
464
|
const runtimeUrl = text(data.url);
|
|
412
465
|
if (status === 503 && data.error === "workspace-runtime did not become ready") {
|
|
413
|
-
return { provisioning: true, retryAfterMs:
|
|
466
|
+
return { provisioning: true, retryAfterMs: 0 };
|
|
467
|
+
}
|
|
468
|
+
if (data.exited === true) throw new SandboxRuntimeBootError(text(data.error) ?? "The workspace runtime exited before it was ready");
|
|
469
|
+
if (status === 410 && isSandboxCheckpointUnusableCause(data.checkpointUnusable)) {
|
|
470
|
+
throw new SandboxCheckpointUnusableError(data.checkpointUnusable, text(data.error) ?? "This workspace's saved files cannot be restored; choose Start fresh.");
|
|
414
471
|
}
|
|
415
472
|
if (status >= 400 || !runtimeUrl) {
|
|
416
|
-
|
|
473
|
+
const reason = `Cloudflare ensure-runtime failed (${status}): ${text(data.error) ?? "no runtime url"}`;
|
|
474
|
+
throw status === 401 || status === 403 ? new SandboxProviderKeyRefusedError(reason) : new Error(reason);
|
|
417
475
|
}
|
|
418
476
|
const targetOut = {
|
|
419
477
|
workspaceId: input.workspaceId,
|
|
@@ -425,10 +483,7 @@ function createCloudflareSandboxDriver(options) {
|
|
|
425
483
|
id: "cloudflare",
|
|
426
484
|
resourceId: sandboxId
|
|
427
485
|
},
|
|
428
|
-
labels:
|
|
429
|
-
...input.labels,
|
|
430
|
-
[WORKSPACE_DIRECTORY_LABEL]: workspaceDirectory(input)
|
|
431
|
-
}
|
|
486
|
+
labels: input.labels
|
|
432
487
|
};
|
|
433
488
|
return targetOut;
|
|
434
489
|
}
|
|
@@ -437,12 +492,10 @@ function createCloudflareSandboxDriver(options) {
|
|
|
437
492
|
// Provider-state enumeration, served by the sandbox Worker's registry
|
|
438
493
|
// (`GET /sandboxes`) rather than by Cloudflare's own APIs.
|
|
439
494
|
//
|
|
440
|
-
// Why a registry is REQUIRED here
|
|
441
|
-
// (@cloudflare/sandbox 0.8.9) rather than assumed:
|
|
495
|
+
// Why a registry is REQUIRED here:
|
|
442
496
|
//
|
|
443
|
-
// 1.
|
|
444
|
-
// get-or-create
|
|
445
|
-
// a namespace. `listDurableObjectIds` is `cloudflare:test`-only.
|
|
497
|
+
// 1. A Durable Object namespace has no enumeration surface: `getByName`
|
|
498
|
+
// is get-or-create, and `listDurableObjectIds` is `cloudflare:test`-only.
|
|
446
499
|
// 2. The account-level REST list-objects endpoint returns
|
|
447
500
|
// `{ id: <hex>, hasStoredData }` and nothing else. `idFromName` is
|
|
448
501
|
// one-way, so a hex id cannot be mapped back to `claxedo-<workspaceId>`
|
|
@@ -455,12 +508,7 @@ function createCloudflareSandboxDriver(options) {
|
|
|
455
508
|
// So the Worker records each sandbox on `ensure-runtime` and drops it on
|
|
456
509
|
// destroy — the pattern Cloudflare recommends for precisely this reason.
|
|
457
510
|
//
|
|
458
|
-
// A
|
|
459
|
-
// 404/501, which THROWS `CloudflareSandboxListingUnsupportedError` rather
|
|
460
|
-
// than returning `[]`. An empty list from an old Worker would mean "nothing
|
|
461
|
-
// is orphaned" and hand GC a silent success — the defect this whole
|
|
462
|
-
// workstream removes. The manager turns that throw back into
|
|
463
|
-
// `listingUnsupported`, so an un-upgraded Worker is loudly visible.
|
|
511
|
+
// A missing registry route or R2 binding cannot establish an empty inventory.
|
|
464
512
|
async list() {
|
|
465
513
|
const deadline = deadlineSignal(timeoutMs);
|
|
466
514
|
try {
|
|
@@ -469,10 +517,14 @@ function createCloudflareSandboxDriver(options) {
|
|
|
469
517
|
headers,
|
|
470
518
|
signal: deadline.signal
|
|
471
519
|
});
|
|
472
|
-
|
|
473
|
-
|
|
520
|
+
if (res.status === 404 || res.status === 501) {
|
|
521
|
+
throw new CloudflareSandboxListingUnsupportedError(`Cloudflare sandbox Worker cannot enumerate sandboxes (${res.status}); requires the /sandboxes registry route and SANDBOX_REGISTRY binding`);
|
|
522
|
+
}
|
|
523
|
+
const data = asRecord(await res.json());
|
|
524
|
+
if (!data) throw new Error("Invalid Cloudflare sandbox listing envelope");
|
|
525
|
+
if (data.supported === false) {
|
|
474
526
|
throw new CloudflareSandboxListingUnsupportedError(
|
|
475
|
-
text(data.error) ?? `Cloudflare sandbox Worker cannot enumerate sandboxes (${res.status}) \u2014 deploy a Worker with the /sandboxes registry route and an R2
|
|
527
|
+
text(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`
|
|
476
528
|
);
|
|
477
529
|
}
|
|
478
530
|
if (!res.ok) {
|
|
@@ -480,7 +532,6 @@ function createCloudflareSandboxDriver(options) {
|
|
|
480
532
|
}
|
|
481
533
|
return registryEntries(data.sandboxes).flatMap((entry) => {
|
|
482
534
|
const sandboxId = entry.sandboxId;
|
|
483
|
-
if (!sandboxId) return [];
|
|
484
535
|
const { sandboxId: _id, ...labels } = entry;
|
|
485
536
|
const target = {
|
|
486
537
|
...labels.workspaceId ? { workspaceId: labels.workspaceId } : {},
|
|
@@ -502,8 +553,9 @@ function createCloudflareSandboxDriver(options) {
|
|
|
502
553
|
},
|
|
503
554
|
metadata: {
|
|
504
555
|
driverRunsIn: ["worker"],
|
|
505
|
-
hostStopBehavior: "
|
|
556
|
+
hostStopBehavior: "terminates-host",
|
|
506
557
|
hostResumeBehavior: "same-host",
|
|
558
|
+
hostWatchesIdle: true,
|
|
507
559
|
targetAccess: "relay",
|
|
508
560
|
secretBrokering: "native",
|
|
509
561
|
egressControl: "none",
|
|
@@ -511,10 +563,24 @@ function createCloudflareSandboxDriver(options) {
|
|
|
511
563
|
},
|
|
512
564
|
ensureHost,
|
|
513
565
|
async touch(target) {
|
|
514
|
-
await call(target.sandboxId, "touch-runtime", { port: runtimePort })
|
|
566
|
+
const { status, data } = await call(target.sandboxId, "touch-runtime", { port: runtimePort });
|
|
567
|
+
if (status < 200 || status >= 300 || data.ok !== true) {
|
|
568
|
+
throw new Error(`Cloudflare touch failed (${status}) for ${target.sandboxId}`);
|
|
569
|
+
}
|
|
515
570
|
},
|
|
516
571
|
async stop(target) {
|
|
517
|
-
|
|
572
|
+
const { status, data } = await call(target.sandboxId, "stop", { epoch: target.epoch });
|
|
573
|
+
if (status >= 400 || data.ok !== true) {
|
|
574
|
+
throw new Error(`Cloudflare stop failed (${status}) for ${target.sandboxId}: ${text(data.error) ?? "not stopped"}`);
|
|
575
|
+
}
|
|
576
|
+
},
|
|
577
|
+
// 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.
|
|
578
|
+
async inspect(target) {
|
|
579
|
+
const { status, data } = await call(target.sandboxId, "inspect", {});
|
|
580
|
+
if (status < 200 || status >= 300 || typeof data.running !== "boolean") {
|
|
581
|
+
throw new Error(`Cloudflare inspect failed (${status}) for ${target.sandboxId}: ${text(data.error) ?? "no container state"}`);
|
|
582
|
+
}
|
|
583
|
+
return data.running ? "serving" : "stopped";
|
|
518
584
|
},
|
|
519
585
|
async destroy(target) {
|
|
520
586
|
const { status } = await call(target.sandboxId, "", {}, "DELETE");
|
|
@@ -523,17 +589,12 @@ function createCloudflareSandboxDriver(options) {
|
|
|
523
589
|
}
|
|
524
590
|
},
|
|
525
591
|
async snapshot(target) {
|
|
526
|
-
const
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
"
|
|
530
|
-
{ directories: captureDirectories(directory) }
|
|
531
|
-
);
|
|
532
|
-
const backupId = text(data.backupId);
|
|
533
|
-
if (status >= 400 || !backupId) {
|
|
534
|
-
throw new Error(`Cloudflare backup failed (${status}): ${text(data.error) ?? "no backup id"}`);
|
|
592
|
+
const { status, data } = await call(target.sandboxId, "snapshot", {});
|
|
593
|
+
const snapshotId = text(data.snapshotId);
|
|
594
|
+
if (status >= 400 || !snapshotId) {
|
|
595
|
+
throw new Error(`Cloudflare snapshot failed (${status}): ${text(data.error) ?? "no snapshot id"}`);
|
|
535
596
|
}
|
|
536
|
-
return { snapshotId
|
|
597
|
+
return { snapshotId };
|
|
537
598
|
}
|
|
538
599
|
};
|
|
539
600
|
}
|
package/dist/drivers/docker.d.ts
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import type { SandboxDriver, SandboxDriverEnsureInput } from "
|
|
1
|
+
import type { SandboxDriver, SandboxDriverEnsureInput } from "../contract";
|
|
2
2
|
type SandboxEnv = Record<string, string | undefined>;
|
|
3
3
|
export type DockerCommandResult = {
|
|
4
4
|
stdout?: string | Buffer;
|
|
5
5
|
stderr?: string | Buffer;
|
|
6
6
|
};
|
|
7
|
-
|
|
7
|
+
/** `env` joins the docker CLI's own environment; a `--env NAME` argument with no value reads NAME from it. */
|
|
8
|
+
export type DockerCommandRunner = (args: string[], timeoutMs?: number, env?: Record<string, string>) => Promise<DockerCommandResult>;
|
|
8
9
|
export type DockerHealthFetch = (input: string, init?: RequestInit) => Promise<Response>;
|
|
9
10
|
export type DockerSandboxDriverOptions = {
|
|
10
11
|
image: string;
|
|
11
12
|
runtimePort?: number;
|
|
12
13
|
runtimeCommand?: string;
|
|
13
14
|
workspaceDir?: string;
|
|
14
|
-
|
|
15
|
+
nativeHarness?: string;
|
|
15
16
|
env?: (input: SandboxDriverEnsureInput, host: {
|
|
16
17
|
id: string;
|
|
17
18
|
}) => Record<string, string> | Promise<Record<string, string>>;
|