@claxedo/sandbox-manager 0.7.0 → 0.9.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 +10 -14
- package/dist/driver-catalog.d.ts +3 -53
- package/dist/driver-catalog.mjs +137 -128
- package/dist/drivers/box.mjs +143 -68
- package/dist/drivers/cloudflare.d.ts +4 -7
- package/dist/drivers/cloudflare.mjs +198 -108
- package/dist/drivers/daytona.d.ts +49 -13
- package/dist/drivers/daytona.mjs +225 -83
- package/dist/drivers/docker.mjs +118 -67
- package/dist/drivers/exe.mjs +74 -51
- package/dist/drivers/fetch-bridge.mjs +19 -4
- package/dist/drivers/modal.d.ts +2 -5
- package/dist/drivers/modal.mjs +130 -75
- package/dist/drivers/transient-error.d.ts +11 -0
- package/dist/drivers/vercel.d.ts +19 -22
- package/dist/drivers/vercel.mjs +178 -104
- package/dist/hosted-network-policy.d.ts +2 -2
- package/dist/hosted-network-policy.mjs +3 -3
- package/dist/image-name.mjs +34 -0
- package/dist/image.d.ts +2 -2
- package/dist/image.mjs +33 -11
- package/dist/index.d.ts +62 -17
- package/dist/index.mjs +68 -22
- package/dist/json.d.ts +13 -0
- package/dist/lease-policy.d.ts +6 -2
- package/dist/lease-policy.mjs +50 -40
- package/dist/lease-types.d.ts +11 -2
- package/dist/runtime-env.d.ts +12 -1
- package/dist/runtime-env.mjs +14 -0
- package/dist/service-url-exposure.d.ts +1 -2
- package/dist/service-url-exposure.mjs +1 -17
- package/dist/stores/memory.mjs +2 -2
- package/package.json +57 -55
- package/dist/drivers/cloudflare-egress.d.ts +0 -40
- package/dist/drivers/cloudflare-egress.mjs +0 -113
|
@@ -27,12 +27,50 @@ var SANDBOX_BASELINE_EGRESS_HOSTS = [
|
|
|
27
27
|
...SANDBOX_PACKAGE_REGISTRY_HOSTS
|
|
28
28
|
];
|
|
29
29
|
|
|
30
|
+
// src/index.ts
|
|
31
|
+
function brokeredSecretPlaceholder(name) {
|
|
32
|
+
return `claxedo-broker:${name}`;
|
|
33
|
+
}
|
|
34
|
+
function brokeredPlaceholderEnv(secrets) {
|
|
35
|
+
return Object.fromEntries((secrets ?? []).map((secret) => [secret.name, brokeredSecretPlaceholder(secret.name)]));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ../sandbox-contract/dist/index.mjs
|
|
39
|
+
var sandboxDriverCredentialFields = {
|
|
40
|
+
exe: [{ key: "api_token", label: "API Token", secret: true }],
|
|
41
|
+
daytona: [{ key: "api_key", label: "API Key", secret: true }],
|
|
42
|
+
modal: [
|
|
43
|
+
{ key: "token_id", label: "Token ID", secret: true },
|
|
44
|
+
{ key: "token_secret", label: "Token Secret", secret: true }
|
|
45
|
+
],
|
|
46
|
+
vercel: [
|
|
47
|
+
{ key: "access_token", label: "Access Token", secret: true },
|
|
48
|
+
{ key: "team_id", label: "Team ID" },
|
|
49
|
+
{ key: "project_id", label: "Project ID" }
|
|
50
|
+
],
|
|
51
|
+
cloudflare: [
|
|
52
|
+
{ key: "api_token", label: "API Token", secret: true },
|
|
53
|
+
{ key: "worker_url", label: "Worker URL" }
|
|
54
|
+
],
|
|
55
|
+
box: [{ key: "api_key", label: "API Key", secret: true }],
|
|
56
|
+
docker: [{ key: "image", label: "Image" }]
|
|
57
|
+
};
|
|
58
|
+
var sandboxDriverLabels = {
|
|
59
|
+
exe: "exe.dev",
|
|
60
|
+
daytona: "Daytona",
|
|
61
|
+
modal: "Modal",
|
|
62
|
+
vercel: "Vercel",
|
|
63
|
+
cloudflare: "Cloudflare",
|
|
64
|
+
box: "Box",
|
|
65
|
+
docker: "Docker"
|
|
66
|
+
};
|
|
67
|
+
|
|
30
68
|
// src/driver-catalog.ts
|
|
31
69
|
var sandboxDriverCatalog = {
|
|
32
70
|
exe: {
|
|
33
71
|
id: "exe",
|
|
34
|
-
label:
|
|
35
|
-
credentialFields:
|
|
72
|
+
label: sandboxDriverLabels.exe,
|
|
73
|
+
credentialFields: sandboxDriverCredentialFields.exe,
|
|
36
74
|
metadata: {
|
|
37
75
|
driverRunsIn: ["worker", "node"],
|
|
38
76
|
hostStopBehavior: "not-supported",
|
|
@@ -52,17 +90,15 @@ var sandboxDriverCatalog = {
|
|
|
52
90
|
},
|
|
53
91
|
daytona: {
|
|
54
92
|
id: "daytona",
|
|
55
|
-
label:
|
|
56
|
-
credentialFields:
|
|
93
|
+
label: sandboxDriverLabels.daytona,
|
|
94
|
+
credentialFields: sandboxDriverCredentialFields.daytona,
|
|
57
95
|
metadata: {
|
|
58
96
|
driverRunsIn: ["worker", "node"],
|
|
59
97
|
hostStopBehavior: "suspends-host",
|
|
60
98
|
hostResumeBehavior: "same-host",
|
|
61
99
|
targetAccess: "relay",
|
|
62
100
|
secretBrokering: "native",
|
|
63
|
-
//
|
|
64
|
-
// by address: `domainAllowList` (names) alongside `networkAllowList`
|
|
65
|
-
// (CIDRs), with `networkBlockAll` as the deny-all floor.
|
|
101
|
+
// The driver translates names to domainAllowList, or addresses to networkAllowList.
|
|
66
102
|
egressControl: "hosts-and-cidrs",
|
|
67
103
|
persistence: {
|
|
68
104
|
resume: "same-sandbox",
|
|
@@ -76,20 +112,15 @@ var sandboxDriverCatalog = {
|
|
|
76
112
|
},
|
|
77
113
|
modal: {
|
|
78
114
|
id: "modal",
|
|
79
|
-
label:
|
|
80
|
-
credentialFields:
|
|
81
|
-
{ key: "token_id", label: "Token ID", secret: true },
|
|
82
|
-
{ key: "token_secret", label: "Token Secret", secret: true }
|
|
83
|
-
],
|
|
115
|
+
label: sandboxDriverLabels.modal,
|
|
116
|
+
credentialFields: sandboxDriverCredentialFields.modal,
|
|
84
117
|
metadata: {
|
|
85
118
|
driverRunsIn: ["node"],
|
|
86
119
|
hostStopBehavior: "terminates-host",
|
|
87
120
|
hostResumeBehavior: "replacement-host",
|
|
88
121
|
targetAccess: "relay",
|
|
89
122
|
secretBrokering: "none",
|
|
90
|
-
//
|
|
91
|
-
// an allowlist, and its driver throws on a host policy. A blackout is
|
|
92
|
-
// not containment for a workspace that has to clone and reach a model.
|
|
123
|
+
// The driver does not install Modal's domain allowlist or alpha sidecar.
|
|
93
124
|
egressControl: "none",
|
|
94
125
|
persistence: {
|
|
95
126
|
resume: "replacement-restore",
|
|
@@ -103,20 +134,15 @@ var sandboxDriverCatalog = {
|
|
|
103
134
|
},
|
|
104
135
|
vercel: {
|
|
105
136
|
id: "vercel",
|
|
106
|
-
label:
|
|
107
|
-
credentialFields:
|
|
108
|
-
{ key: "access_token", label: "Access Token", secret: true },
|
|
109
|
-
{ key: "team_id", label: "Team ID" },
|
|
110
|
-
{ key: "project_id", label: "Project ID" }
|
|
111
|
-
],
|
|
137
|
+
label: sandboxDriverLabels.vercel,
|
|
138
|
+
credentialFields: sandboxDriverCredentialFields.vercel,
|
|
112
139
|
metadata: {
|
|
113
140
|
driverRunsIn: ["node"],
|
|
114
141
|
hostStopBehavior: "terminates-host",
|
|
115
142
|
hostResumeBehavior: "replacement-host",
|
|
116
143
|
targetAccess: "relay",
|
|
117
144
|
secretBrokering: "native",
|
|
118
|
-
//
|
|
119
|
-
// never CIDRs.
|
|
145
|
+
// The driver translates hosts only; the SDK's subnet rules are not wired to net.cidrs.
|
|
120
146
|
egressControl: "hosts",
|
|
121
147
|
persistence: {
|
|
122
148
|
resume: "replacement-restore",
|
|
@@ -130,23 +156,15 @@ var sandboxDriverCatalog = {
|
|
|
130
156
|
},
|
|
131
157
|
cloudflare: {
|
|
132
158
|
id: "cloudflare",
|
|
133
|
-
label:
|
|
134
|
-
credentialFields:
|
|
135
|
-
{ key: "api_token", label: "API Token", secret: true },
|
|
136
|
-
{ key: "worker_url", label: "Worker URL" }
|
|
137
|
-
],
|
|
159
|
+
label: sandboxDriverLabels.cloudflare,
|
|
160
|
+
credentialFields: sandboxDriverCredentialFields.cloudflare,
|
|
138
161
|
metadata: {
|
|
139
162
|
driverRunsIn: ["worker"],
|
|
140
163
|
hostStopBehavior: "not-supported",
|
|
141
164
|
hostResumeBehavior: "same-host",
|
|
142
165
|
targetAccess: "relay",
|
|
143
|
-
secretBrokering: "
|
|
144
|
-
//
|
|
145
|
-
// (drivers/cloudflare-egress.ts) is an OPT-IN proxy the sandbox chooses
|
|
146
|
-
// to route brokered-credential requests through, not a boundary. The
|
|
147
|
-
// driver therefore drops `net` entirely — which is exactly why this
|
|
148
|
-
// must be declared, so the manager refuses instead of provisioning a
|
|
149
|
-
// sandbox the caller believes is contained.
|
|
166
|
+
secretBrokering: "native",
|
|
167
|
+
// Credential handlers do not restrict unrelated destinations.
|
|
150
168
|
egressControl: "none",
|
|
151
169
|
persistence: {
|
|
152
170
|
resume: "replacement-restore",
|
|
@@ -160,8 +178,8 @@ var sandboxDriverCatalog = {
|
|
|
160
178
|
},
|
|
161
179
|
box: {
|
|
162
180
|
id: "box",
|
|
163
|
-
label:
|
|
164
|
-
credentialFields:
|
|
181
|
+
label: sandboxDriverLabels.box,
|
|
182
|
+
credentialFields: sandboxDriverCredentialFields.box,
|
|
165
183
|
metadata: {
|
|
166
184
|
driverRunsIn: ["node"],
|
|
167
185
|
hostStopBehavior: "suspends-host",
|
|
@@ -181,8 +199,8 @@ var sandboxDriverCatalog = {
|
|
|
181
199
|
},
|
|
182
200
|
docker: {
|
|
183
201
|
id: "docker",
|
|
184
|
-
label:
|
|
185
|
-
credentialFields:
|
|
202
|
+
label: sandboxDriverLabels.docker,
|
|
203
|
+
credentialFields: sandboxDriverCredentialFields.docker,
|
|
186
204
|
metadata: {
|
|
187
205
|
driverRunsIn: ["local"],
|
|
188
206
|
hostStopBehavior: "terminates-host",
|
|
@@ -202,7 +220,33 @@ var sandboxDriverCatalog = {
|
|
|
202
220
|
}
|
|
203
221
|
};
|
|
204
222
|
|
|
223
|
+
// ../claxedo-helpers/dist/guards.mjs
|
|
224
|
+
function isRecord(value) {
|
|
225
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
226
|
+
}
|
|
227
|
+
function asRecord(value) {
|
|
228
|
+
return isRecord(value) ? value : void 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/json.ts
|
|
232
|
+
function text(input) {
|
|
233
|
+
return typeof input === "string" && input.length > 0 ? input : void 0;
|
|
234
|
+
}
|
|
235
|
+
|
|
205
236
|
// src/runtime-env.ts
|
|
237
|
+
function workspaceRuntimeBootEnv(input) {
|
|
238
|
+
const env = {
|
|
239
|
+
...workspaceRuntimeTargetEnv(input),
|
|
240
|
+
...workspaceRuntimeSourceEnv(input),
|
|
241
|
+
...input.env
|
|
242
|
+
};
|
|
243
|
+
if (input.runner) env.WORKSPACE_RUNTIME_RUNNER = input.runner;
|
|
244
|
+
if (input.controlEnv?.relayJwksUrl) env.WORKSPACE_RUNTIME_RELAY_JWKS_URL = input.controlEnv.relayJwksUrl;
|
|
245
|
+
if (input.controlEnv?.relayVerifyPem) env.WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM = input.controlEnv.relayVerifyPem;
|
|
246
|
+
if (input.controlEnv?.managementJwksUrl) env.WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL = input.controlEnv.managementJwksUrl;
|
|
247
|
+
if (input.controlEnv?.sessionAuthorityUrl) env.WORKSPACE_RUNTIME_SESSION_AUTHORITY_URL = input.controlEnv.sessionAuthorityUrl;
|
|
248
|
+
return env;
|
|
249
|
+
}
|
|
206
250
|
function workspaceRuntimeTargetEnv(input) {
|
|
207
251
|
return {
|
|
208
252
|
WORKSPACE_RUNTIME_WORKSPACE_ID: input.workspaceId,
|
|
@@ -222,10 +266,33 @@ function workspaceRuntimeSourceEnv(input) {
|
|
|
222
266
|
}
|
|
223
267
|
|
|
224
268
|
// src/drivers/cloudflare.ts
|
|
269
|
+
function registryEntries(input) {
|
|
270
|
+
if (!Array.isArray(input)) return [];
|
|
271
|
+
return input.flatMap((item) => {
|
|
272
|
+
const row = asRecord(item);
|
|
273
|
+
if (!row) return [];
|
|
274
|
+
const entries = Object.entries(row).filter((entry) => typeof entry[1] === "string");
|
|
275
|
+
return [Object.fromEntries(entries)];
|
|
276
|
+
});
|
|
277
|
+
}
|
|
225
278
|
var DEFAULT_WORKSPACE_DIR = "/workspace";
|
|
226
279
|
var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
|
|
227
280
|
var DEFAULT_TIMEOUT_MS = 45e3;
|
|
281
|
+
var DEFAULT_ENSURE_TIMEOUT_MS = 3e5;
|
|
228
282
|
var WORKSPACE_DIRECTORY_LABEL = "claxedo.workspaceDirectory";
|
|
283
|
+
function deadlineSignal(timeoutMs) {
|
|
284
|
+
const controller = new AbortController();
|
|
285
|
+
const timer = setTimeout(
|
|
286
|
+
() => controller.abort(new DOMException("The operation timed out", "TimeoutError")),
|
|
287
|
+
timeoutMs
|
|
288
|
+
);
|
|
289
|
+
return {
|
|
290
|
+
signal: controller.signal,
|
|
291
|
+
cleanup() {
|
|
292
|
+
clearTimeout(timer);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
}
|
|
229
296
|
var RUNTIME_DATA_DIR = "/var/lib/claxedo";
|
|
230
297
|
function captureDirectories(workspaceDirectory) {
|
|
231
298
|
return [workspaceDirectory, RUNTIME_DATA_DIR];
|
|
@@ -249,6 +316,7 @@ function createCloudflareSandboxDriver(options) {
|
|
|
249
316
|
const runtimeCommand = options.runtimeCommand ?? DEFAULT_RUNTIME_COMMAND;
|
|
250
317
|
const workspaceDir = options.workspaceDir ?? DEFAULT_WORKSPACE_DIR;
|
|
251
318
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
319
|
+
const ensureTimeoutMs = options.timeoutMs ?? DEFAULT_ENSURE_TIMEOUT_MS;
|
|
252
320
|
const base = cleanUrl(options.workerUrl);
|
|
253
321
|
const headers = {
|
|
254
322
|
Authorization: `Bearer ${options.apiToken}`,
|
|
@@ -256,51 +324,64 @@ function createCloudflareSandboxDriver(options) {
|
|
|
256
324
|
};
|
|
257
325
|
async function call(sandboxId, action, body, method = "POST") {
|
|
258
326
|
const url = `${base}/sandbox/${encodeURIComponent(sandboxId)}${action ? `/${action}` : ""}`;
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
327
|
+
const deadline = deadlineSignal(action === "ensure-runtime" ? ensureTimeoutMs : timeoutMs);
|
|
328
|
+
try {
|
|
329
|
+
const res = await doFetch(url, {
|
|
330
|
+
method,
|
|
331
|
+
headers,
|
|
332
|
+
...method === "POST" ? { body: JSON.stringify(body) } : {},
|
|
333
|
+
signal: deadline.signal
|
|
334
|
+
});
|
|
335
|
+
const data = asRecord(await res.json().catch(() => ({}))) ?? {};
|
|
336
|
+
return { status: res.status, data };
|
|
337
|
+
} finally {
|
|
338
|
+
deadline.cleanup();
|
|
339
|
+
}
|
|
267
340
|
}
|
|
268
341
|
function workspaceDirectory(input) {
|
|
269
342
|
return input.workspaceRoot ?? workspaceDir;
|
|
270
343
|
}
|
|
271
344
|
async function bootEnv(input, hostId) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
345
|
+
return workspaceRuntimeBootEnv({
|
|
346
|
+
workspaceId: input.workspaceId,
|
|
347
|
+
hostId,
|
|
348
|
+
directory: workspaceDirectory(input),
|
|
349
|
+
port: runtimePort,
|
|
350
|
+
host: "0.0.0.0",
|
|
351
|
+
source: input.source,
|
|
352
|
+
env: {
|
|
353
|
+
// Must match the checkpoint capture set below.
|
|
354
|
+
CLAXEDO_DATA_DIR: RUNTIME_DATA_DIR,
|
|
355
|
+
...input.env,
|
|
356
|
+
...await options.env?.(input, { id: hostId }),
|
|
357
|
+
// Last, so no caller-supplied variable of the same name can stand in
|
|
358
|
+
// for a placeholder the outbound handler matches on.
|
|
359
|
+
...brokeredPlaceholderEnv(input.secrets)
|
|
360
|
+
},
|
|
361
|
+
runner: options.runner,
|
|
362
|
+
controlEnv: options.controlEnv
|
|
363
|
+
});
|
|
291
364
|
}
|
|
292
365
|
function egressRegistrations(input) {
|
|
293
|
-
if (
|
|
366
|
+
if (input.secrets === void 0) return void 0;
|
|
294
367
|
return input.secrets.map((secret) => {
|
|
295
368
|
if (!secret.header) {
|
|
296
369
|
throw new Error(
|
|
297
|
-
`cloudflare brokered secret "${secret.name}" requires a header
|
|
370
|
+
`cloudflare brokered secret "${secret.name}" requires a header for native outbound injection`
|
|
298
371
|
);
|
|
299
372
|
}
|
|
300
373
|
if (secret.hosts.length === 0) {
|
|
301
374
|
throw new Error(`cloudflare brokered secret "${secret.name}" requires at least one host in its egress allowlist`);
|
|
302
375
|
}
|
|
303
|
-
|
|
376
|
+
const value = secret.scheme ? `${secret.scheme} ${secret.value}` : secret.value;
|
|
377
|
+
return {
|
|
378
|
+
name: secret.name,
|
|
379
|
+
hosts: secret.hosts,
|
|
380
|
+
header: secret.header,
|
|
381
|
+
value,
|
|
382
|
+
methods: [...secret.methods ?? []],
|
|
383
|
+
pathPrefixes: [...secret.pathPrefixes ?? []]
|
|
384
|
+
};
|
|
304
385
|
});
|
|
305
386
|
}
|
|
306
387
|
async function ensureHost(input) {
|
|
@@ -327,14 +408,17 @@ function createCloudflareSandboxDriver(options) {
|
|
|
327
408
|
});
|
|
328
409
|
if (!response) return { provisioning: true, retryAfterMs: 2e3 };
|
|
329
410
|
const { status, data } = response;
|
|
330
|
-
|
|
331
|
-
if (status
|
|
332
|
-
|
|
411
|
+
const runtimeUrl = text(data.url);
|
|
412
|
+
if (status === 503 && data.error === "workspace-runtime did not become ready") {
|
|
413
|
+
return { provisioning: true, retryAfterMs: 2e3 };
|
|
414
|
+
}
|
|
415
|
+
if (status >= 400 || !runtimeUrl) {
|
|
416
|
+
throw new Error(`Cloudflare ensure-runtime failed (${status}): ${text(data.error) ?? "no runtime url"}`);
|
|
333
417
|
}
|
|
334
418
|
const targetOut = {
|
|
335
419
|
workspaceId: input.workspaceId,
|
|
336
420
|
sandboxId,
|
|
337
|
-
url:
|
|
421
|
+
url: runtimeUrl,
|
|
338
422
|
hostId,
|
|
339
423
|
driverResourceId: sandboxId,
|
|
340
424
|
driver: {
|
|
@@ -378,45 +462,50 @@ function createCloudflareSandboxDriver(options) {
|
|
|
378
462
|
// workstream removes. The manager turns that throw back into
|
|
379
463
|
// `listingUnsupported`, so an un-upgraded Worker is loudly visible.
|
|
380
464
|
async list() {
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
465
|
+
const deadline = deadlineSignal(timeoutMs);
|
|
466
|
+
try {
|
|
467
|
+
const res = await doFetch(`${base}/sandboxes`, {
|
|
468
|
+
method: "GET",
|
|
469
|
+
headers,
|
|
470
|
+
signal: deadline.signal
|
|
471
|
+
});
|
|
472
|
+
const data = asRecord(await res.json().catch(() => ({}))) ?? {};
|
|
473
|
+
if (res.status === 404 || res.status === 501 || data.supported === false) {
|
|
474
|
+
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 BACKUP_BUCKET binding`
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
if (!res.ok) {
|
|
479
|
+
throw new Error(`Cloudflare sandbox listing failed (${res.status}): ${text(data.error) ?? "unknown error"}`);
|
|
480
|
+
}
|
|
481
|
+
return registryEntries(data.sandboxes).flatMap((entry) => {
|
|
482
|
+
const sandboxId = entry.sandboxId;
|
|
483
|
+
if (!sandboxId) return [];
|
|
484
|
+
const { sandboxId: _id, ...labels } = entry;
|
|
485
|
+
const target = {
|
|
486
|
+
...labels.workspaceId ? { workspaceId: labels.workspaceId } : {},
|
|
487
|
+
sandboxId,
|
|
488
|
+
// Identity, not a reachable address — GC only names what it destroys.
|
|
489
|
+
url: `${base}/sandbox/${encodeURIComponent(sandboxId)}/proxy`,
|
|
490
|
+
// Matches `ensureHost`'s `hostId = sandboxId`, so a live sandbox
|
|
491
|
+
// cannot fail GC's identity check and be destroyed as an orphan.
|
|
492
|
+
hostId: sandboxId,
|
|
493
|
+
driverResourceId: sandboxId,
|
|
494
|
+
labels,
|
|
495
|
+
driver: { id: "cloudflare", resourceId: sandboxId }
|
|
496
|
+
};
|
|
497
|
+
return [target];
|
|
498
|
+
});
|
|
499
|
+
} finally {
|
|
500
|
+
deadline.cleanup();
|
|
394
501
|
}
|
|
395
|
-
return (data.sandboxes ?? []).flatMap((entry) => {
|
|
396
|
-
const sandboxId = entry.sandboxId;
|
|
397
|
-
if (!sandboxId) return [];
|
|
398
|
-
const { sandboxId: _id, ...labels } = entry;
|
|
399
|
-
const target = {
|
|
400
|
-
...labels.workspaceId ? { workspaceId: labels.workspaceId } : {},
|
|
401
|
-
sandboxId,
|
|
402
|
-
// Identity, not a reachable address — GC only names what it destroys.
|
|
403
|
-
url: `${base}/sandbox/${encodeURIComponent(sandboxId)}/proxy`,
|
|
404
|
-
// Matches `ensureHost`'s `hostId = sandboxId`, so a live sandbox
|
|
405
|
-
// cannot fail GC's identity check and be destroyed as an orphan.
|
|
406
|
-
hostId: sandboxId,
|
|
407
|
-
driverResourceId: sandboxId,
|
|
408
|
-
labels,
|
|
409
|
-
driver: { id: "cloudflare", resourceId: sandboxId }
|
|
410
|
-
};
|
|
411
|
-
return [target];
|
|
412
|
-
});
|
|
413
502
|
},
|
|
414
503
|
metadata: {
|
|
415
504
|
driverRunsIn: ["worker"],
|
|
416
505
|
hostStopBehavior: "not-supported",
|
|
417
506
|
hostResumeBehavior: "same-host",
|
|
418
507
|
targetAccess: "relay",
|
|
419
|
-
secretBrokering: "
|
|
508
|
+
secretBrokering: "native",
|
|
420
509
|
egressControl: "none",
|
|
421
510
|
persistence: sandboxDriverCatalog.cloudflare.metadata.persistence
|
|
422
511
|
},
|
|
@@ -440,10 +529,11 @@ function createCloudflareSandboxDriver(options) {
|
|
|
440
529
|
"backup",
|
|
441
530
|
{ directories: captureDirectories(directory) }
|
|
442
531
|
);
|
|
443
|
-
|
|
444
|
-
|
|
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"}`);
|
|
445
535
|
}
|
|
446
|
-
return { snapshotId:
|
|
536
|
+
return { snapshotId: backupId };
|
|
447
537
|
}
|
|
448
538
|
};
|
|
449
539
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SandboxDriver, SandboxDriverEnsureInput } from "..";
|
|
2
|
+
import { type WorkspaceRuntimeControlEnv } from "../runtime-env";
|
|
2
3
|
export type DaytonaSandboxLike = {
|
|
3
4
|
id: string;
|
|
4
5
|
state?: string;
|
|
@@ -18,6 +19,7 @@ export type DaytonaSandboxLike = {
|
|
|
18
19
|
start: (timeout?: number) => Promise<void>;
|
|
19
20
|
stop: (timeout?: number) => Promise<void>;
|
|
20
21
|
delete: (timeout?: number) => Promise<void>;
|
|
22
|
+
updateSecrets: (secrets: Record<string, string>) => Promise<void>;
|
|
21
23
|
/**
|
|
22
24
|
* Replace the sandbox's outbound egress policy in place, without stopping it
|
|
23
25
|
* — the Daytona SDK's `Sandbox.updateNetworkSettings`, which drives the same
|
|
@@ -75,17 +77,44 @@ export type DaytonaClientLike = {
|
|
|
75
77
|
}) => Promise<DaytonaSandboxLike>;
|
|
76
78
|
get: (sandboxIdOrName: string) => Promise<DaytonaSandboxLike>;
|
|
77
79
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* (
|
|
81
|
-
* `
|
|
82
|
-
*
|
|
80
|
+
* The org-secret service, in the SDK's own shape so `createDefaultClient`
|
|
81
|
+
* hands `sdk.secret` straight through. Upsert and withdrawal POLICY lives in
|
|
82
|
+
* this driver (`reconcileBrokeredSecrets`) rather than behind a
|
|
83
|
+
* `upsertSecret` client method: `create` is a conflict on an existing name,
|
|
84
|
+
* so a client-level "upsert" is a second implementation of the lookup every
|
|
85
|
+
* embedder would have to get right.
|
|
83
86
|
*/
|
|
84
|
-
|
|
87
|
+
secret?: DaytonaSecretServiceLike;
|
|
88
|
+
};
|
|
89
|
+
/** Identity of an org secret. The plaintext value is write-only and never returned. */
|
|
90
|
+
export type DaytonaSecretLike = {
|
|
91
|
+
id: string;
|
|
92
|
+
name: string;
|
|
93
|
+
};
|
|
94
|
+
export type DaytonaSecretServiceLike = {
|
|
95
|
+
/**
|
|
96
|
+
* `name` is a PARTIAL match, and pages are cursor-based; callers filter the
|
|
97
|
+
* page themselves and follow `nextCursor` until it is null.
|
|
98
|
+
*/
|
|
99
|
+
list: (query?: {
|
|
100
|
+
name?: string;
|
|
101
|
+
cursor?: string;
|
|
102
|
+
limit?: number;
|
|
103
|
+
}) => Promise<{
|
|
104
|
+
items: DaytonaSecretLike[];
|
|
105
|
+
nextCursor?: string | null;
|
|
106
|
+
}>;
|
|
107
|
+
/** Throws `DaytonaConflictError` when a secret of that name already exists. */
|
|
108
|
+
create: (params: {
|
|
85
109
|
name: string;
|
|
86
110
|
value: string;
|
|
87
|
-
hosts
|
|
88
|
-
}) => Promise<
|
|
111
|
+
hosts?: string[];
|
|
112
|
+
}) => Promise<DaytonaSecretLike>;
|
|
113
|
+
update: (secretId: string, params: {
|
|
114
|
+
value?: string;
|
|
115
|
+
hosts?: string[];
|
|
116
|
+
}) => Promise<unknown>;
|
|
117
|
+
delete: (secretId: string) => Promise<void>;
|
|
89
118
|
};
|
|
90
119
|
export type DaytonaSandboxDriverOptions = {
|
|
91
120
|
/** Daytona API base URL. Defaults to the SDK default. */
|
|
@@ -107,11 +136,7 @@ export type DaytonaSandboxDriverOptions = {
|
|
|
107
136
|
/** Default runner injected as WORKSPACE_RUNTIME_RUNNER. */
|
|
108
137
|
runner?: string;
|
|
109
138
|
/** Static control-plane config injected so the runtime verifies relay-proxied requests. */
|
|
110
|
-
controlEnv?:
|
|
111
|
-
relayJwksUrl?: string;
|
|
112
|
-
relayVerifyPem?: string;
|
|
113
|
-
managementJwksUrl?: string;
|
|
114
|
-
};
|
|
139
|
+
controlEnv?: WorkspaceRuntimeControlEnv;
|
|
115
140
|
/** Dynamic runtime env that needs the acquired sandbox id or current lease. */
|
|
116
141
|
env?: (input: SandboxDriverEnsureInput, sandbox: DaytonaSandboxLike) => Record<string, string> | Promise<Record<string, string>>;
|
|
117
142
|
/** Auto-stop / auto-delete policy, in minutes. */
|
|
@@ -123,5 +148,16 @@ export type DaytonaSandboxDriverOptions = {
|
|
|
123
148
|
operationTimeoutSeconds?: number;
|
|
124
149
|
/** Injected for tests. */
|
|
125
150
|
client?: DaytonaClientLike;
|
|
151
|
+
/**
|
|
152
|
+
* Sink for the two operator-visible events this driver has no other way to
|
|
153
|
+
* report: a restart it had to force on a live sandbox, and an org secret
|
|
154
|
+
* under this workspace's prefix that it did not mint and therefore emptied
|
|
155
|
+
* without mounting.
|
|
156
|
+
*
|
|
157
|
+
* Defaults to `console.warn`, deliberately unconditional — the same
|
|
158
|
+
* reasoning as `onEgressUnenforced` in index.ts. Pass your own to route them
|
|
159
|
+
* into a logger; pass `() => {}` only if you have another way to see them.
|
|
160
|
+
*/
|
|
161
|
+
warn?: (message: string) => void;
|
|
126
162
|
};
|
|
127
163
|
export declare function createDaytonaSandboxDriver(options: DaytonaSandboxDriverOptions): SandboxDriver;
|