@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
package/dist/drivers/daytona.mjs
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
// src/drivers/daytona.ts
|
|
2
|
-
import { Daytona } from "@daytona/sdk";
|
|
3
|
-
|
|
4
1
|
// src/daytona-allow-list.ts
|
|
5
2
|
function formatDaytonaAllowList(cidrs) {
|
|
6
3
|
return cidrs.slice(0, 10).join(",");
|
|
@@ -10,6 +7,19 @@ function formatDaytonaDomainAllowList(hosts) {
|
|
|
10
7
|
}
|
|
11
8
|
|
|
12
9
|
// src/runtime-env.ts
|
|
10
|
+
function workspaceRuntimeBootEnv(input) {
|
|
11
|
+
const env = {
|
|
12
|
+
...workspaceRuntimeTargetEnv(input),
|
|
13
|
+
...workspaceRuntimeSourceEnv(input),
|
|
14
|
+
...input.env
|
|
15
|
+
};
|
|
16
|
+
if (input.runner) env.WORKSPACE_RUNTIME_RUNNER = input.runner;
|
|
17
|
+
if (input.controlEnv?.relayJwksUrl) env.WORKSPACE_RUNTIME_RELAY_JWKS_URL = input.controlEnv.relayJwksUrl;
|
|
18
|
+
if (input.controlEnv?.relayVerifyPem) env.WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM = input.controlEnv.relayVerifyPem;
|
|
19
|
+
if (input.controlEnv?.managementJwksUrl) env.WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL = input.controlEnv.managementJwksUrl;
|
|
20
|
+
if (input.controlEnv?.sessionAuthorityUrl) env.WORKSPACE_RUNTIME_SESSION_AUTHORITY_URL = input.controlEnv.sessionAuthorityUrl;
|
|
21
|
+
return env;
|
|
22
|
+
}
|
|
13
23
|
function workspaceRuntimeTargetEnv(input) {
|
|
14
24
|
return {
|
|
15
25
|
WORKSPACE_RUNTIME_WORKSPACE_ID: input.workspaceId,
|
|
@@ -62,12 +72,42 @@ var SANDBOX_BASELINE_EGRESS_HOSTS = [
|
|
|
62
72
|
...SANDBOX_PACKAGE_REGISTRY_HOSTS
|
|
63
73
|
];
|
|
64
74
|
|
|
75
|
+
// ../sandbox-contract/dist/index.mjs
|
|
76
|
+
var sandboxDriverCredentialFields = {
|
|
77
|
+
exe: [{ key: "api_token", label: "API Token", secret: true }],
|
|
78
|
+
daytona: [{ key: "api_key", label: "API Key", secret: true }],
|
|
79
|
+
modal: [
|
|
80
|
+
{ key: "token_id", label: "Token ID", secret: true },
|
|
81
|
+
{ key: "token_secret", label: "Token Secret", secret: true }
|
|
82
|
+
],
|
|
83
|
+
vercel: [
|
|
84
|
+
{ key: "access_token", label: "Access Token", secret: true },
|
|
85
|
+
{ key: "team_id", label: "Team ID" },
|
|
86
|
+
{ key: "project_id", label: "Project ID" }
|
|
87
|
+
],
|
|
88
|
+
cloudflare: [
|
|
89
|
+
{ key: "api_token", label: "API Token", secret: true },
|
|
90
|
+
{ key: "worker_url", label: "Worker URL" }
|
|
91
|
+
],
|
|
92
|
+
box: [{ key: "api_key", label: "API Key", secret: true }],
|
|
93
|
+
docker: [{ key: "image", label: "Image" }]
|
|
94
|
+
};
|
|
95
|
+
var sandboxDriverLabels = {
|
|
96
|
+
exe: "exe.dev",
|
|
97
|
+
daytona: "Daytona",
|
|
98
|
+
modal: "Modal",
|
|
99
|
+
vercel: "Vercel",
|
|
100
|
+
cloudflare: "Cloudflare",
|
|
101
|
+
box: "Box",
|
|
102
|
+
docker: "Docker"
|
|
103
|
+
};
|
|
104
|
+
|
|
65
105
|
// src/driver-catalog.ts
|
|
66
106
|
var sandboxDriverCatalog = {
|
|
67
107
|
exe: {
|
|
68
108
|
id: "exe",
|
|
69
|
-
label:
|
|
70
|
-
credentialFields:
|
|
109
|
+
label: sandboxDriverLabels.exe,
|
|
110
|
+
credentialFields: sandboxDriverCredentialFields.exe,
|
|
71
111
|
metadata: {
|
|
72
112
|
driverRunsIn: ["worker", "node"],
|
|
73
113
|
hostStopBehavior: "not-supported",
|
|
@@ -87,17 +127,15 @@ var sandboxDriverCatalog = {
|
|
|
87
127
|
},
|
|
88
128
|
daytona: {
|
|
89
129
|
id: "daytona",
|
|
90
|
-
label:
|
|
91
|
-
credentialFields:
|
|
130
|
+
label: sandboxDriverLabels.daytona,
|
|
131
|
+
credentialFields: sandboxDriverCredentialFields.daytona,
|
|
92
132
|
metadata: {
|
|
93
133
|
driverRunsIn: ["worker", "node"],
|
|
94
134
|
hostStopBehavior: "suspends-host",
|
|
95
135
|
hostResumeBehavior: "same-host",
|
|
96
136
|
targetAccess: "relay",
|
|
97
137
|
secretBrokering: "native",
|
|
98
|
-
//
|
|
99
|
-
// by address: `domainAllowList` (names) alongside `networkAllowList`
|
|
100
|
-
// (CIDRs), with `networkBlockAll` as the deny-all floor.
|
|
138
|
+
// The driver translates names to domainAllowList, or addresses to networkAllowList.
|
|
101
139
|
egressControl: "hosts-and-cidrs",
|
|
102
140
|
persistence: {
|
|
103
141
|
resume: "same-sandbox",
|
|
@@ -111,20 +149,15 @@ var sandboxDriverCatalog = {
|
|
|
111
149
|
},
|
|
112
150
|
modal: {
|
|
113
151
|
id: "modal",
|
|
114
|
-
label:
|
|
115
|
-
credentialFields:
|
|
116
|
-
{ key: "token_id", label: "Token ID", secret: true },
|
|
117
|
-
{ key: "token_secret", label: "Token Secret", secret: true }
|
|
118
|
-
],
|
|
152
|
+
label: sandboxDriverLabels.modal,
|
|
153
|
+
credentialFields: sandboxDriverCredentialFields.modal,
|
|
119
154
|
metadata: {
|
|
120
155
|
driverRunsIn: ["node"],
|
|
121
156
|
hostStopBehavior: "terminates-host",
|
|
122
157
|
hostResumeBehavior: "replacement-host",
|
|
123
158
|
targetAccess: "relay",
|
|
124
159
|
secretBrokering: "none",
|
|
125
|
-
//
|
|
126
|
-
// an allowlist, and its driver throws on a host policy. A blackout is
|
|
127
|
-
// not containment for a workspace that has to clone and reach a model.
|
|
160
|
+
// The driver does not install Modal's domain allowlist or alpha sidecar.
|
|
128
161
|
egressControl: "none",
|
|
129
162
|
persistence: {
|
|
130
163
|
resume: "replacement-restore",
|
|
@@ -138,20 +171,15 @@ var sandboxDriverCatalog = {
|
|
|
138
171
|
},
|
|
139
172
|
vercel: {
|
|
140
173
|
id: "vercel",
|
|
141
|
-
label:
|
|
142
|
-
credentialFields:
|
|
143
|
-
{ key: "access_token", label: "Access Token", secret: true },
|
|
144
|
-
{ key: "team_id", label: "Team ID" },
|
|
145
|
-
{ key: "project_id", label: "Project ID" }
|
|
146
|
-
],
|
|
174
|
+
label: sandboxDriverLabels.vercel,
|
|
175
|
+
credentialFields: sandboxDriverCredentialFields.vercel,
|
|
147
176
|
metadata: {
|
|
148
177
|
driverRunsIn: ["node"],
|
|
149
178
|
hostStopBehavior: "terminates-host",
|
|
150
179
|
hostResumeBehavior: "replacement-host",
|
|
151
180
|
targetAccess: "relay",
|
|
152
181
|
secretBrokering: "native",
|
|
153
|
-
//
|
|
154
|
-
// never CIDRs.
|
|
182
|
+
// The driver translates hosts only; the SDK's subnet rules are not wired to net.cidrs.
|
|
155
183
|
egressControl: "hosts",
|
|
156
184
|
persistence: {
|
|
157
185
|
resume: "replacement-restore",
|
|
@@ -165,23 +193,15 @@ var sandboxDriverCatalog = {
|
|
|
165
193
|
},
|
|
166
194
|
cloudflare: {
|
|
167
195
|
id: "cloudflare",
|
|
168
|
-
label:
|
|
169
|
-
credentialFields:
|
|
170
|
-
{ key: "api_token", label: "API Token", secret: true },
|
|
171
|
-
{ key: "worker_url", label: "Worker URL" }
|
|
172
|
-
],
|
|
196
|
+
label: sandboxDriverLabels.cloudflare,
|
|
197
|
+
credentialFields: sandboxDriverCredentialFields.cloudflare,
|
|
173
198
|
metadata: {
|
|
174
199
|
driverRunsIn: ["worker"],
|
|
175
200
|
hostStopBehavior: "not-supported",
|
|
176
201
|
hostResumeBehavior: "same-host",
|
|
177
202
|
targetAccess: "relay",
|
|
178
|
-
secretBrokering: "
|
|
179
|
-
//
|
|
180
|
-
// (drivers/cloudflare-egress.ts) is an OPT-IN proxy the sandbox chooses
|
|
181
|
-
// to route brokered-credential requests through, not a boundary. The
|
|
182
|
-
// driver therefore drops `net` entirely — which is exactly why this
|
|
183
|
-
// must be declared, so the manager refuses instead of provisioning a
|
|
184
|
-
// sandbox the caller believes is contained.
|
|
203
|
+
secretBrokering: "native",
|
|
204
|
+
// Credential handlers do not restrict unrelated destinations.
|
|
185
205
|
egressControl: "none",
|
|
186
206
|
persistence: {
|
|
187
207
|
resume: "replacement-restore",
|
|
@@ -195,8 +215,8 @@ var sandboxDriverCatalog = {
|
|
|
195
215
|
},
|
|
196
216
|
box: {
|
|
197
217
|
id: "box",
|
|
198
|
-
label:
|
|
199
|
-
credentialFields:
|
|
218
|
+
label: sandboxDriverLabels.box,
|
|
219
|
+
credentialFields: sandboxDriverCredentialFields.box,
|
|
200
220
|
metadata: {
|
|
201
221
|
driverRunsIn: ["node"],
|
|
202
222
|
hostStopBehavior: "suspends-host",
|
|
@@ -216,8 +236,8 @@ var sandboxDriverCatalog = {
|
|
|
216
236
|
},
|
|
217
237
|
docker: {
|
|
218
238
|
id: "docker",
|
|
219
|
-
label:
|
|
220
|
-
credentialFields:
|
|
239
|
+
label: sandboxDriverLabels.docker,
|
|
240
|
+
credentialFields: sandboxDriverCredentialFields.docker,
|
|
221
241
|
metadata: {
|
|
222
242
|
driverRunsIn: ["local"],
|
|
223
243
|
hostStopBehavior: "terminates-host",
|
|
@@ -237,28 +257,87 @@ var sandboxDriverCatalog = {
|
|
|
237
257
|
}
|
|
238
258
|
};
|
|
239
259
|
|
|
260
|
+
// ../claxedo-helpers/dist/guards.mjs
|
|
261
|
+
function isRecord(value) {
|
|
262
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/json.ts
|
|
266
|
+
function num(input) {
|
|
267
|
+
return typeof input === "number" ? input : void 0;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/drivers/transient-error.ts
|
|
271
|
+
function textOf(input) {
|
|
272
|
+
return typeof input === "string" ? input : "";
|
|
273
|
+
}
|
|
274
|
+
function driverErrorSignals(err) {
|
|
275
|
+
const shaped = isRecord(err) ? err : {};
|
|
276
|
+
const nested = isRecord(shaped.response) ? shaped.response : {};
|
|
277
|
+
return {
|
|
278
|
+
status: num(nested.status) ?? num(shaped.status),
|
|
279
|
+
text: `${textOf(shaped.code)} ${textOf(shaped.name)} ${textOf(shaped.message)}`.toLowerCase()
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function isTransientDriverError(err, markers) {
|
|
283
|
+
const { status, text } = driverErrorSignals(err);
|
|
284
|
+
if (status !== void 0 && status >= 500) return true;
|
|
285
|
+
return markers.some((marker) => text.includes(marker));
|
|
286
|
+
}
|
|
287
|
+
|
|
240
288
|
// src/drivers/daytona.ts
|
|
241
289
|
var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
|
|
242
290
|
var DEFAULT_WORKSPACE_DIR = "/workspace";
|
|
243
291
|
var DEFAULT_PREVIEW_EXPIRY_S = 3600;
|
|
244
292
|
var DEFAULT_OPERATION_TIMEOUT_S = 60;
|
|
245
293
|
var LIST_PAGE_SIZE = 100;
|
|
294
|
+
var SECRET_LIST_PAGE_SIZE = 200;
|
|
295
|
+
var SENTINEL_SECRET_ENV = "CLAXEDO_BROKERED_SECRET_SLOT";
|
|
296
|
+
var REVOKED_SECRET_VALUE = "claxedo-revoked";
|
|
246
297
|
var MAX_LIST_PAGES = 100;
|
|
247
298
|
function labelName(workspaceId) {
|
|
248
299
|
return `claxedo-${workspaceId}`;
|
|
249
300
|
}
|
|
301
|
+
function encodeSecretSegment(value) {
|
|
302
|
+
return encodeURIComponent(value).replace(/[-_.!~*'()]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`).replace(/%/g, "_");
|
|
303
|
+
}
|
|
304
|
+
function workspaceSecretPrefix(workspaceId) {
|
|
305
|
+
return `claxedo-${encodeSecretSegment(workspaceId)}-`;
|
|
306
|
+
}
|
|
250
307
|
function daytonaSecretName(workspaceId, secretName) {
|
|
251
|
-
|
|
252
|
-
|
|
308
|
+
return `${workspaceSecretPrefix(workspaceId)}${encodeSecretSegment(secretName)}`;
|
|
309
|
+
}
|
|
310
|
+
function daytonaSecretEnvName(workspaceId, secretName) {
|
|
311
|
+
try {
|
|
312
|
+
return decodeURIComponent(secretName.slice(workspaceSecretPrefix(workspaceId).length).replace(/_/g, "%"));
|
|
313
|
+
} catch {
|
|
314
|
+
return void 0;
|
|
315
|
+
}
|
|
253
316
|
}
|
|
317
|
+
async function listWorkspaceSecrets(secrets, workspaceId) {
|
|
318
|
+
const prefix = workspaceSecretPrefix(workspaceId);
|
|
319
|
+
const held = /* @__PURE__ */ new Map();
|
|
320
|
+
let cursor;
|
|
321
|
+
for (let page = 0; page < MAX_LIST_PAGES; page++) {
|
|
322
|
+
const result = await secrets.list({ name: prefix, limit: SECRET_LIST_PAGE_SIZE, ...cursor ? { cursor } : {} });
|
|
323
|
+
for (const secret of result.items ?? []) {
|
|
324
|
+
if (secret.name.startsWith(prefix)) held.set(secret.name, secret);
|
|
325
|
+
}
|
|
326
|
+
cursor = result.nextCursor ?? void 0;
|
|
327
|
+
if (!cursor) break;
|
|
328
|
+
}
|
|
329
|
+
return held;
|
|
330
|
+
}
|
|
331
|
+
async function withdrawSecret(secrets, secret, options) {
|
|
332
|
+
await secrets.update(secret.id, { value: REVOKED_SECRET_VALUE, hosts: [] });
|
|
333
|
+
if (options.delete) await secrets.delete(secret.id);
|
|
334
|
+
}
|
|
335
|
+
var TRANSIENT_MARKERS = ["timeout", "pending", "starting"];
|
|
254
336
|
function transientDriverError(err) {
|
|
255
|
-
|
|
256
|
-
const status = shaped.response?.status ?? shaped.status;
|
|
257
|
-
if (typeof status === "number" && status >= 500) return true;
|
|
258
|
-
const text = `${shaped.name ?? ""} ${shaped.message ?? ""}`.toLowerCase();
|
|
259
|
-
return text.includes("timeout") || text.includes("pending") || text.includes("starting");
|
|
337
|
+
return isTransientDriverError(err, TRANSIENT_MARKERS);
|
|
260
338
|
}
|
|
261
|
-
function createDefaultClient(options) {
|
|
339
|
+
async function createDefaultClient(options) {
|
|
340
|
+
const { Daytona } = await import("@daytona/sdk");
|
|
262
341
|
const sdk = new Daytona({
|
|
263
342
|
apiKey: options.apiKey,
|
|
264
343
|
...options.apiUrl ? { apiUrl: options.apiUrl } : {},
|
|
@@ -285,37 +364,36 @@ function createDefaultClient(options) {
|
|
|
285
364
|
return sdk.create(params, operation);
|
|
286
365
|
},
|
|
287
366
|
get: (sandboxIdOrName) => sdk.get(sandboxIdOrName),
|
|
288
|
-
|
|
289
|
-
await sdk.secret.create({ name, value, hosts });
|
|
290
|
-
}
|
|
367
|
+
secret: sdk.secret
|
|
291
368
|
};
|
|
292
369
|
}
|
|
293
370
|
function createDaytonaSandboxDriver(options) {
|
|
294
|
-
|
|
371
|
+
let defaultClient;
|
|
372
|
+
function resolveClient() {
|
|
373
|
+
if (options.client) return options.client;
|
|
374
|
+
defaultClient ??= createDefaultClient(options);
|
|
375
|
+
return defaultClient;
|
|
376
|
+
}
|
|
295
377
|
const runtimePort = options.runtimePort ?? DEFAULT_WORKSPACE_RUNTIME_PORT;
|
|
296
378
|
const runtimeCommand = options.runtimeCommand ?? DEFAULT_RUNTIME_COMMAND;
|
|
297
379
|
const workspaceDir = options.workspaceDir ?? DEFAULT_WORKSPACE_DIR;
|
|
298
380
|
const previewExpiry = options.previewExpirySeconds ?? DEFAULT_PREVIEW_EXPIRY_S;
|
|
299
381
|
const operationTimeout = options.operationTimeoutSeconds ?? DEFAULT_OPERATION_TIMEOUT_S;
|
|
382
|
+
const warn = options.warn ?? ((message) => console.warn(message));
|
|
300
383
|
function workspaceDirectory(input) {
|
|
301
384
|
return input.workspaceRoot ?? workspaceDir;
|
|
302
385
|
}
|
|
303
386
|
function staticBootEnv(input, hostId, directory) {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
};
|
|
314
|
-
if (options.runner) env.WORKSPACE_RUNTIME_RUNNER = options.runner;
|
|
315
|
-
if (options.controlEnv?.relayJwksUrl) env.WORKSPACE_RUNTIME_RELAY_JWKS_URL = options.controlEnv.relayJwksUrl;
|
|
316
|
-
if (options.controlEnv?.relayVerifyPem) env.WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM = options.controlEnv.relayVerifyPem;
|
|
317
|
-
if (options.controlEnv?.managementJwksUrl) env.WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL = options.controlEnv.managementJwksUrl;
|
|
318
|
-
return env;
|
|
387
|
+
return workspaceRuntimeBootEnv({
|
|
388
|
+
workspaceId: input.workspaceId,
|
|
389
|
+
hostId,
|
|
390
|
+
directory,
|
|
391
|
+
port: runtimePort,
|
|
392
|
+
source: input.source,
|
|
393
|
+
env: input.env,
|
|
394
|
+
runner: options.runner,
|
|
395
|
+
controlEnv: options.controlEnv
|
|
396
|
+
});
|
|
319
397
|
}
|
|
320
398
|
async function bootEnv(input, sandbox, hostId) {
|
|
321
399
|
return {
|
|
@@ -324,13 +402,14 @@ function createDaytonaSandboxDriver(options) {
|
|
|
324
402
|
};
|
|
325
403
|
}
|
|
326
404
|
async function findExisting(workspaceId) {
|
|
405
|
+
const client = await resolveClient();
|
|
327
406
|
const labels = { "claxedo.workspaceId": workspaceId };
|
|
328
407
|
if (client.findByLabels) return client.findByLabels(labels).catch(() => void 0);
|
|
329
408
|
const result = await client.list?.(labels, 1, 1).catch(() => void 0);
|
|
330
409
|
return result?.items?.[0];
|
|
331
410
|
}
|
|
332
411
|
async function sandboxById(sandboxId) {
|
|
333
|
-
return
|
|
412
|
+
return (await resolveClient()).get(sandboxId);
|
|
334
413
|
}
|
|
335
414
|
async function previewUrl(sandbox) {
|
|
336
415
|
const signed = await sandbox.getSignedPreviewUrl(runtimePort, previewExpiry).catch(() => void 0);
|
|
@@ -366,21 +445,67 @@ function createDaytonaSandboxDriver(options) {
|
|
|
366
445
|
};
|
|
367
446
|
return target;
|
|
368
447
|
}
|
|
369
|
-
async function
|
|
370
|
-
|
|
371
|
-
if (!
|
|
448
|
+
async function brokeredSecretService() {
|
|
449
|
+
const secrets = (await resolveClient()).secret;
|
|
450
|
+
if (!secrets) {
|
|
372
451
|
throw new Error("daytona sandbox client does not support secret brokering");
|
|
373
452
|
}
|
|
453
|
+
return secrets;
|
|
454
|
+
}
|
|
455
|
+
async function reconcileBrokeredSecrets(input, options2) {
|
|
456
|
+
const secrets = await brokeredSecretService();
|
|
457
|
+
const existing = await listWorkspaceSecrets(secrets, input.workspaceId);
|
|
374
458
|
const references = {};
|
|
375
|
-
|
|
459
|
+
const desired = /* @__PURE__ */ new Set();
|
|
460
|
+
const sentinel = daytonaSecretName(input.workspaceId, SENTINEL_SECRET_ENV);
|
|
461
|
+
desired.add(sentinel);
|
|
462
|
+
references[SENTINEL_SECRET_ENV] = sentinel;
|
|
463
|
+
if (!existing.has(sentinel)) await secrets.create({ name: sentinel, value: "", hosts: [] });
|
|
464
|
+
for (const secret of input.secrets ?? []) {
|
|
376
465
|
if (secret.hosts.length === 0) {
|
|
377
466
|
throw new Error(`daytona brokered secret "${secret.name}" requires at least one host in its egress allowlist`);
|
|
378
467
|
}
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
references[secret.name] =
|
|
468
|
+
const name = daytonaSecretName(input.workspaceId, secret.name);
|
|
469
|
+
desired.add(name);
|
|
470
|
+
references[secret.name] = name;
|
|
471
|
+
const current = existing.get(name);
|
|
472
|
+
if (current) await secrets.update(current.id, { value: secret.value, hosts: secret.hosts });
|
|
473
|
+
else await secrets.create({ name, value: secret.value, hosts: secret.hosts });
|
|
474
|
+
}
|
|
475
|
+
if (options2.withdraw) {
|
|
476
|
+
for (const [name, secret] of existing) {
|
|
477
|
+
if (desired.has(name)) continue;
|
|
478
|
+
await withdrawSecret(secrets, secret, { delete: false });
|
|
479
|
+
if (!options2.mountWithdrawn) continue;
|
|
480
|
+
const envName = daytonaSecretEnvName(input.workspaceId, name);
|
|
481
|
+
if (!envName) {
|
|
482
|
+
warn(
|
|
483
|
+
`[sandbox-manager] daytona org secret ${name} matches workspace ${input.workspaceId}'s prefix but was not minted by this driver; it was emptied and left unmounted`
|
|
484
|
+
);
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
desired.add(name);
|
|
488
|
+
references[envName] = name;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
const mountedNamesChanged = existing.size !== desired.size || [...desired].some((name) => !existing.has(name));
|
|
492
|
+
return { references, mountedNamesChanged };
|
|
493
|
+
}
|
|
494
|
+
function mayBeRunning(sandbox) {
|
|
495
|
+
return !sandbox.state || sandbox.state === "started";
|
|
496
|
+
}
|
|
497
|
+
async function restartForMountedSecrets(sandbox, workspaceId) {
|
|
498
|
+
warn(
|
|
499
|
+
`[sandbox-manager] restarting daytona sandbox ${sandbox.id} (workspace ${workspaceId}): the set of brokered secret names changed and mounted env vars only reach processes spawned after the change`
|
|
500
|
+
);
|
|
501
|
+
await sandbox.stop(operationTimeout);
|
|
502
|
+
await sandbox.start(operationTimeout);
|
|
503
|
+
}
|
|
504
|
+
async function applyBrokeredSecrets(sandbox, plan, workspaceId) {
|
|
505
|
+
await sandbox.updateSecrets(plan.references);
|
|
506
|
+
if (plan.mountedNamesChanged && mayBeRunning(sandbox)) {
|
|
507
|
+
await restartForMountedSecrets(sandbox, workspaceId);
|
|
382
508
|
}
|
|
383
|
-
return references;
|
|
384
509
|
}
|
|
385
510
|
async function applyNetworkPolicy(sandbox, input) {
|
|
386
511
|
const net = network(input);
|
|
@@ -402,13 +527,16 @@ function createDaytonaSandboxDriver(options) {
|
|
|
402
527
|
const hostId = labelName(input.workspaceId);
|
|
403
528
|
const bootSource = input.bootSource?.kind === "image" ? { image: input.bootSource.image } : { snapshot: input.bootSource?.kind === "driver-snapshot" ? input.bootSource.snapshotId : input.snapshot ?? options.baseSnapshot };
|
|
404
529
|
const net = network(input);
|
|
405
|
-
const secrets = await brokeredSecretReferences(input);
|
|
406
530
|
const existing = await findExisting(input.workspaceId);
|
|
407
|
-
const
|
|
531
|
+
const plan = existing && input.secrets === void 0 ? void 0 : await reconcileBrokeredSecrets(input, {
|
|
532
|
+
withdraw: input.secrets !== void 0,
|
|
533
|
+
mountWithdrawn: Boolean(existing)
|
|
534
|
+
});
|
|
535
|
+
const sandbox = existing ?? await (await resolveClient()).create({
|
|
408
536
|
name: labelName(input.workspaceId),
|
|
409
537
|
...bootSource,
|
|
410
538
|
envVars: staticBootEnv(input, hostId, workspaceDirectory(input)),
|
|
411
|
-
...
|
|
539
|
+
...plan ? { secrets: plan.references } : {},
|
|
412
540
|
labels: { ...input.labels, "claxedo.workspaceId": input.workspaceId },
|
|
413
541
|
public: false,
|
|
414
542
|
...net,
|
|
@@ -419,6 +547,7 @@ function createDaytonaSandboxDriver(options) {
|
|
|
419
547
|
throw err;
|
|
420
548
|
});
|
|
421
549
|
if (!sandbox) return { provisioning: true, retryAfterMs: 2e3 };
|
|
550
|
+
if (existing && plan) await applyBrokeredSecrets(existing, plan, input.workspaceId);
|
|
422
551
|
if (!await ensureStarted(sandbox)) return { provisioning: true, retryAfterMs: 2e3 };
|
|
423
552
|
if (existing && await applyNetworkPolicy(existing, input) === "retry") {
|
|
424
553
|
return { provisioning: true, retryAfterMs: 2e3 };
|
|
@@ -463,6 +592,7 @@ function createDaytonaSandboxDriver(options) {
|
|
|
463
592
|
// round-trip and GC only needs identity, so a sweep over N orphans would
|
|
464
593
|
// cost N extra API calls to fill a field it then discards.
|
|
465
594
|
async list() {
|
|
595
|
+
const client = await resolveClient();
|
|
466
596
|
if (!client.list) {
|
|
467
597
|
throw new Error("daytona sandbox client does not support listing");
|
|
468
598
|
}
|
|
@@ -496,6 +626,10 @@ function createDaytonaSandboxDriver(options) {
|
|
|
496
626
|
},
|
|
497
627
|
async resumeHost(input) {
|
|
498
628
|
const sandbox = await sandboxById(input.lease.sandboxId);
|
|
629
|
+
if (input.ensure.secrets !== void 0) {
|
|
630
|
+
const plan = await reconcileBrokeredSecrets(input.ensure, { withdraw: true, mountWithdrawn: true });
|
|
631
|
+
await applyBrokeredSecrets(sandbox, plan, input.ensure.workspaceId);
|
|
632
|
+
}
|
|
499
633
|
if (!await ensureStarted(sandbox)) return { provisioning: true, retryAfterMs: 2e3 };
|
|
500
634
|
if (await applyNetworkPolicy(sandbox, input.ensure) === "retry") {
|
|
501
635
|
return { provisioning: true, retryAfterMs: 2e3 };
|
|
@@ -513,9 +647,17 @@ function createDaytonaSandboxDriver(options) {
|
|
|
513
647
|
},
|
|
514
648
|
async destroy(target) {
|
|
515
649
|
await sandboxById(target.sandboxId).then((sandbox) => sandbox.delete(operationTimeout)).catch((err) => {
|
|
516
|
-
|
|
517
|
-
if (status !== 404) throw err;
|
|
650
|
+
if (driverErrorSignals(err).status !== 404) throw err;
|
|
518
651
|
});
|
|
652
|
+
if (!target.workspaceId) {
|
|
653
|
+
throw new Error(
|
|
654
|
+
`daytona deleted sandbox ${target.sandboxId} but cannot withdraw its brokered secrets: the destroy target names no workspace`
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
const secrets = await brokeredSecretService();
|
|
658
|
+
for (const secret of (await listWorkspaceSecrets(secrets, target.workspaceId)).values()) {
|
|
659
|
+
await withdrawSecret(secrets, secret, { delete: true });
|
|
660
|
+
}
|
|
519
661
|
},
|
|
520
662
|
async snapshot(target) {
|
|
521
663
|
const sandbox = await sandboxById(target.sandboxId);
|