@claxedo/sandbox-manager 0.6.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.
Files changed (41) hide show
  1. package/README.md +60 -15
  2. package/dist/checkpoint-manager.d.ts +47 -0
  3. package/dist/checkpoint-manager.mjs +116 -0
  4. package/dist/daytona-allow-list.d.ts +11 -0
  5. package/dist/daytona-allow-list.mjs +5 -1
  6. package/dist/driver-catalog.d.ts +5 -52
  7. package/dist/driver-catalog.mjs +263 -121
  8. package/dist/drivers/box.mjs +309 -39
  9. package/dist/drivers/cloudflare.d.ts +18 -7
  10. package/dist/drivers/cloudflare.mjs +423 -34
  11. package/dist/drivers/daytona.d.ts +82 -13
  12. package/dist/drivers/daytona.mjs +495 -45
  13. package/dist/drivers/docker.mjs +284 -38
  14. package/dist/drivers/exe.d.ts +26 -0
  15. package/dist/drivers/exe.mjs +492 -0
  16. package/dist/drivers/fetch-bridge.mjs +29 -5
  17. package/dist/drivers/modal.d.ts +2 -5
  18. package/dist/drivers/modal.mjs +296 -46
  19. package/dist/drivers/transient-error.d.ts +11 -0
  20. package/dist/drivers/vercel.d.ts +27 -11
  21. package/dist/drivers/vercel.mjs +339 -59
  22. package/dist/hosted-network-policy.d.ts +87 -0
  23. package/dist/hosted-network-policy.mjs +61 -0
  24. package/dist/image-name.mjs +34 -0
  25. package/dist/image.d.ts +18 -3
  26. package/dist/image.mjs +47 -32
  27. package/dist/index.d.ts +281 -17
  28. package/dist/index.mjs +393 -26
  29. package/dist/json.d.ts +13 -0
  30. package/dist/lease-policy.d.ts +6 -3
  31. package/dist/lease-policy.mjs +213 -54
  32. package/dist/lease-types.d.ts +17 -3
  33. package/dist/runtime-env.d.ts +12 -1
  34. package/dist/runtime-env.mjs +14 -0
  35. package/dist/runtime-version.mjs +1 -9
  36. package/dist/service-url-exposure.d.ts +1 -2
  37. package/dist/service-url-exposure.mjs +1 -17
  38. package/dist/stores/memory.mjs +39 -4
  39. package/package.json +67 -51
  40. package/dist/drivers/cloudflare-egress.d.ts +0 -40
  41. package/dist/drivers/cloudflare-egress.mjs +0 -113
@@ -1,12 +1,25 @@
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(",");
7
4
  }
5
+ function formatDaytonaDomainAllowList(hosts) {
6
+ return [...new Set(hosts)].join(",");
7
+ }
8
8
 
9
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
+ }
10
23
  function workspaceRuntimeTargetEnv(input) {
11
24
  return {
12
25
  WORKSPACE_RUNTIME_WORKSPACE_ID: input.workspaceId,
@@ -33,26 +46,298 @@ function shell(input) {
33
46
  // src/constants.ts
34
47
  var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
35
48
 
49
+ // src/hosted-network-policy.ts
50
+ var SANDBOX_MODEL_PROVIDER_HOSTS = [
51
+ "api.anthropic.com",
52
+ "api.openai.com",
53
+ "generativelanguage.googleapis.com",
54
+ "openrouter.ai",
55
+ // Model catalog + engine config, fetched on runner startup.
56
+ "models.dev",
57
+ "opencode.ai"
58
+ ];
59
+ var SANDBOX_PACKAGE_REGISTRY_HOSTS = [
60
+ "registry.npmjs.org",
61
+ "nodejs.org",
62
+ "pypi.org",
63
+ "files.pythonhosted.org",
64
+ "crates.io",
65
+ "static.crates.io",
66
+ "index.crates.io",
67
+ "proxy.golang.org",
68
+ "sum.golang.org"
69
+ ];
70
+ var SANDBOX_BASELINE_EGRESS_HOSTS = [
71
+ ...SANDBOX_MODEL_PROVIDER_HOSTS,
72
+ ...SANDBOX_PACKAGE_REGISTRY_HOSTS
73
+ ];
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
+
105
+ // src/driver-catalog.ts
106
+ var sandboxDriverCatalog = {
107
+ exe: {
108
+ id: "exe",
109
+ label: sandboxDriverLabels.exe,
110
+ credentialFields: sandboxDriverCredentialFields.exe,
111
+ metadata: {
112
+ driverRunsIn: ["worker", "node"],
113
+ hostStopBehavior: "not-supported",
114
+ hostResumeBehavior: "same-host",
115
+ targetAccess: "relay",
116
+ secretBrokering: "none",
117
+ egressControl: "none",
118
+ persistence: {
119
+ resume: "same-sandbox",
120
+ capture: "none",
121
+ clone: true,
122
+ captureSource: "not-applicable",
123
+ retention: "provider-managed",
124
+ restoreMount: "same-resource"
125
+ }
126
+ }
127
+ },
128
+ daytona: {
129
+ id: "daytona",
130
+ label: sandboxDriverLabels.daytona,
131
+ credentialFields: sandboxDriverCredentialFields.daytona,
132
+ metadata: {
133
+ driverRunsIn: ["worker", "node"],
134
+ hostStopBehavior: "suspends-host",
135
+ hostResumeBehavior: "same-host",
136
+ targetAccess: "relay",
137
+ secretBrokering: "native",
138
+ // The driver translates names to domainAllowList, or addresses to networkAllowList.
139
+ egressControl: "hosts-and-cidrs",
140
+ persistence: {
141
+ resume: "same-sandbox",
142
+ capture: "filesystem",
143
+ clone: false,
144
+ captureSource: "preserved",
145
+ retention: "provider-managed",
146
+ restoreMount: "new-resource"
147
+ }
148
+ }
149
+ },
150
+ modal: {
151
+ id: "modal",
152
+ label: sandboxDriverLabels.modal,
153
+ credentialFields: sandboxDriverCredentialFields.modal,
154
+ metadata: {
155
+ driverRunsIn: ["node"],
156
+ hostStopBehavior: "terminates-host",
157
+ hostResumeBehavior: "replacement-host",
158
+ targetAccess: "relay",
159
+ secretBrokering: "none",
160
+ // The driver does not install Modal's domain allowlist or alpha sidecar.
161
+ egressControl: "none",
162
+ persistence: {
163
+ resume: "replacement-restore",
164
+ capture: "filesystem",
165
+ clone: false,
166
+ captureSource: "preserved",
167
+ retention: "provider-managed",
168
+ restoreMount: "new-resource"
169
+ }
170
+ }
171
+ },
172
+ vercel: {
173
+ id: "vercel",
174
+ label: sandboxDriverLabels.vercel,
175
+ credentialFields: sandboxDriverCredentialFields.vercel,
176
+ metadata: {
177
+ driverRunsIn: ["node"],
178
+ hostStopBehavior: "terminates-host",
179
+ hostResumeBehavior: "replacement-host",
180
+ targetAccess: "relay",
181
+ secretBrokering: "native",
182
+ // The driver translates hosts only; the SDK's subnet rules are not wired to net.cidrs.
183
+ egressControl: "hosts",
184
+ persistence: {
185
+ resume: "replacement-restore",
186
+ capture: "filesystem",
187
+ clone: false,
188
+ captureSource: "stopped",
189
+ retention: "explicit",
190
+ restoreMount: "new-resource"
191
+ }
192
+ }
193
+ },
194
+ cloudflare: {
195
+ id: "cloudflare",
196
+ label: sandboxDriverLabels.cloudflare,
197
+ credentialFields: sandboxDriverCredentialFields.cloudflare,
198
+ metadata: {
199
+ driverRunsIn: ["worker"],
200
+ hostStopBehavior: "not-supported",
201
+ hostResumeBehavior: "same-host",
202
+ targetAccess: "relay",
203
+ secretBrokering: "native",
204
+ // Credential handlers do not restrict unrelated destinations.
205
+ egressControl: "none",
206
+ persistence: {
207
+ resume: "replacement-restore",
208
+ capture: "directories",
209
+ clone: false,
210
+ captureSource: "preserved",
211
+ retention: "provider-managed",
212
+ restoreMount: "copy-on-write"
213
+ }
214
+ }
215
+ },
216
+ box: {
217
+ id: "box",
218
+ label: sandboxDriverLabels.box,
219
+ credentialFields: sandboxDriverCredentialFields.box,
220
+ metadata: {
221
+ driverRunsIn: ["node"],
222
+ hostStopBehavior: "suspends-host",
223
+ hostResumeBehavior: "same-host",
224
+ targetAccess: "relay",
225
+ secretBrokering: "none",
226
+ egressControl: "none",
227
+ persistence: {
228
+ resume: "same-sandbox",
229
+ capture: "none",
230
+ clone: false,
231
+ captureSource: "not-applicable",
232
+ retention: "provider-managed",
233
+ restoreMount: "same-resource"
234
+ }
235
+ }
236
+ },
237
+ docker: {
238
+ id: "docker",
239
+ label: sandboxDriverLabels.docker,
240
+ credentialFields: sandboxDriverCredentialFields.docker,
241
+ metadata: {
242
+ driverRunsIn: ["local"],
243
+ hostStopBehavior: "terminates-host",
244
+ hostResumeBehavior: "same-host",
245
+ targetAccess: "loopback",
246
+ secretBrokering: "none",
247
+ egressControl: "none",
248
+ persistence: {
249
+ resume: "replacement-restore",
250
+ capture: "same-resource",
251
+ clone: false,
252
+ captureSource: "preserved",
253
+ retention: "provider-managed",
254
+ restoreMount: "same-resource"
255
+ }
256
+ }
257
+ }
258
+ };
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
+
36
288
  // src/drivers/daytona.ts
37
289
  var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
38
290
  var DEFAULT_WORKSPACE_DIR = "/workspace";
39
291
  var DEFAULT_PREVIEW_EXPIRY_S = 3600;
40
292
  var DEFAULT_OPERATION_TIMEOUT_S = 60;
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";
297
+ var MAX_LIST_PAGES = 100;
41
298
  function labelName(workspaceId) {
42
299
  return `claxedo-${workspaceId}`;
43
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
+ }
44
307
  function daytonaSecretName(workspaceId, secretName) {
45
- const safe = (value) => value.replace(/[^a-zA-Z0-9_-]+/g, "-");
46
- return `claxedo-${safe(workspaceId)}-${safe(secretName)}`;
308
+ return `${workspaceSecretPrefix(workspaceId)}${encodeSecretSegment(secretName)}`;
47
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
+ }
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"];
48
336
  function transientDriverError(err) {
49
- const shaped = err;
50
- const status = shaped.response?.status ?? shaped.status;
51
- if (typeof status === "number" && status >= 500) return true;
52
- const text = `${shaped.name ?? ""} ${shaped.message ?? ""}`.toLowerCase();
53
- return text.includes("timeout") || text.includes("pending") || text.includes("starting");
337
+ return isTransientDriverError(err, TRANSIENT_MARKERS);
54
338
  }
55
- function createDefaultClient(options) {
339
+ async function createDefaultClient(options) {
340
+ const { Daytona } = await import("@daytona/sdk");
56
341
  const sdk = new Daytona({
57
342
  apiKey: options.apiKey,
58
343
  ...options.apiUrl ? { apiUrl: options.apiUrl } : {},
@@ -65,42 +350,50 @@ function createDefaultClient(options) {
65
350
  for await (const sandbox of sdk.list({ labels })) return sandbox;
66
351
  return void 0;
67
352
  },
353
+ // Page 1 carries everything; later pages are empty. See DaytonaClientLike.list.
354
+ async list(labels, page = 1, limit) {
355
+ if (page > 1) return { items: [] };
356
+ const items = [];
357
+ for await (const sandbox of sdk.list({ ...labels ? { labels } : {}, ...limit ? { limit } : {} })) {
358
+ items.push(sandbox);
359
+ }
360
+ return { items };
361
+ },
68
362
  create(params, operation) {
69
363
  if (params.image) return sdk.create({ ...params, image: params.image }, operation);
70
364
  return sdk.create(params, operation);
71
365
  },
72
366
  get: (sandboxIdOrName) => sdk.get(sandboxIdOrName),
73
- async upsertSecret({ name, value, hosts }) {
74
- await sdk.secret.create({ name, value, hosts });
75
- }
367
+ secret: sdk.secret
76
368
  };
77
369
  }
78
370
  function createDaytonaSandboxDriver(options) {
79
- const client = options.client ?? createDefaultClient(options);
371
+ let defaultClient;
372
+ function resolveClient() {
373
+ if (options.client) return options.client;
374
+ defaultClient ??= createDefaultClient(options);
375
+ return defaultClient;
376
+ }
80
377
  const runtimePort = options.runtimePort ?? DEFAULT_WORKSPACE_RUNTIME_PORT;
81
378
  const runtimeCommand = options.runtimeCommand ?? DEFAULT_RUNTIME_COMMAND;
82
379
  const workspaceDir = options.workspaceDir ?? DEFAULT_WORKSPACE_DIR;
83
380
  const previewExpiry = options.previewExpirySeconds ?? DEFAULT_PREVIEW_EXPIRY_S;
84
381
  const operationTimeout = options.operationTimeoutSeconds ?? DEFAULT_OPERATION_TIMEOUT_S;
382
+ const warn = options.warn ?? ((message) => console.warn(message));
85
383
  function workspaceDirectory(input) {
86
384
  return input.workspaceRoot ?? workspaceDir;
87
385
  }
88
386
  function staticBootEnv(input, hostId, directory) {
89
- const env = {
90
- ...workspaceRuntimeTargetEnv({
91
- workspaceId: input.workspaceId,
92
- hostId,
93
- directory,
94
- port: runtimePort
95
- }),
96
- ...workspaceRuntimeSourceEnv({ source: input.source }),
97
- ...input.env
98
- };
99
- if (options.runner) env.WORKSPACE_RUNTIME_RUNNER = options.runner;
100
- if (options.controlEnv?.relayJwksUrl) env.WORKSPACE_RUNTIME_RELAY_JWKS_URL = options.controlEnv.relayJwksUrl;
101
- if (options.controlEnv?.relayVerifyPem) env.WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM = options.controlEnv.relayVerifyPem;
102
- if (options.controlEnv?.managementJwksUrl) env.WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL = options.controlEnv.managementJwksUrl;
103
- 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
+ });
104
397
  }
105
398
  async function bootEnv(input, sandbox, hostId) {
106
399
  return {
@@ -109,13 +402,14 @@ function createDaytonaSandboxDriver(options) {
109
402
  };
110
403
  }
111
404
  async function findExisting(workspaceId) {
405
+ const client = await resolveClient();
112
406
  const labels = { "claxedo.workspaceId": workspaceId };
113
407
  if (client.findByLabels) return client.findByLabels(labels).catch(() => void 0);
114
408
  const result = await client.list?.(labels, 1, 1).catch(() => void 0);
115
409
  return result?.items?.[0];
116
410
  }
117
411
  async function sandboxById(sandboxId) {
118
- return client.get(sandboxId);
412
+ return (await resolveClient()).get(sandboxId);
119
413
  }
120
414
  async function previewUrl(sandbox) {
121
415
  const signed = await sandbox.getSignedPreviewUrl(runtimePort, previewExpiry).catch(() => void 0);
@@ -151,32 +445,98 @@ function createDaytonaSandboxDriver(options) {
151
445
  };
152
446
  return target;
153
447
  }
154
- async function brokeredSecretReferences(input) {
155
- if (!input.secrets?.length) return {};
156
- if (!client.upsertSecret) {
448
+ async function brokeredSecretService() {
449
+ const secrets = (await resolveClient()).secret;
450
+ if (!secrets) {
157
451
  throw new Error("daytona sandbox client does not support secret brokering");
158
452
  }
453
+ return secrets;
454
+ }
455
+ async function reconcileBrokeredSecrets(input, options2) {
456
+ const secrets = await brokeredSecretService();
457
+ const existing = await listWorkspaceSecrets(secrets, input.workspaceId);
159
458
  const references = {};
160
- for (const secret of input.secrets) {
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 ?? []) {
161
465
  if (secret.hosts.length === 0) {
162
466
  throw new Error(`daytona brokered secret "${secret.name}" requires at least one host in its egress allowlist`);
163
467
  }
164
- const secretName = daytonaSecretName(input.workspaceId, secret.name);
165
- await client.upsertSecret({ name: secretName, value: secret.value, hosts: secret.hosts });
166
- references[secret.name] = secretName;
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);
508
+ }
509
+ }
510
+ async function applyNetworkPolicy(sandbox, input) {
511
+ const net = network(input);
512
+ if (Object.keys(net).length === 0) return "applied";
513
+ if (!sandbox.updateNetworkSettings) {
514
+ throw new Error(
515
+ `daytona sandbox client cannot reapply egress policy to existing sandbox ${sandbox.id}: the client does not support updateNetworkSettings, so the requested restricted policy cannot be guaranteed on reuse \u2014 refusing rather than serving the creation-time policy`
516
+ );
517
+ }
518
+ try {
519
+ await sandbox.updateNetworkSettings(net);
520
+ } catch (err) {
521
+ if (transientDriverError(err)) return "retry";
522
+ throw err;
167
523
  }
168
- return references;
524
+ return "applied";
169
525
  }
170
526
  async function ensureHost(input) {
171
527
  const hostId = labelName(input.workspaceId);
172
528
  const bootSource = input.bootSource?.kind === "image" ? { image: input.bootSource.image } : { snapshot: input.bootSource?.kind === "driver-snapshot" ? input.bootSource.snapshotId : input.snapshot ?? options.baseSnapshot };
173
529
  const net = network(input);
174
- const secrets = await brokeredSecretReferences(input);
175
- const sandbox = await findExisting(input.workspaceId) ?? await client.create({
530
+ const existing = await findExisting(input.workspaceId);
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({
176
536
  name: labelName(input.workspaceId),
177
537
  ...bootSource,
178
538
  envVars: staticBootEnv(input, hostId, workspaceDirectory(input)),
179
- ...Object.keys(secrets).length ? { secrets } : {},
539
+ ...plan ? { secrets: plan.references } : {},
180
540
  labels: { ...input.labels, "claxedo.workspaceId": input.workspaceId },
181
541
  public: false,
182
542
  ...net,
@@ -187,7 +547,11 @@ function createDaytonaSandboxDriver(options) {
187
547
  throw err;
188
548
  });
189
549
  if (!sandbox) return { provisioning: true, retryAfterMs: 2e3 };
550
+ if (existing && plan) await applyBrokeredSecrets(existing, plan, input.workspaceId);
190
551
  if (!await ensureStarted(sandbox)) return { provisioning: true, retryAfterMs: 2e3 };
552
+ if (existing && await applyNetworkPolicy(existing, input) === "retry") {
553
+ return { provisioning: true, retryAfterMs: 2e3 };
554
+ }
191
555
  return readyTarget(input, sandbox, hostId);
192
556
  }
193
557
  return {
@@ -197,12 +561,79 @@ function createDaytonaSandboxDriver(options) {
197
561
  hostStopBehavior: "suspends-host",
198
562
  hostResumeBehavior: "same-host",
199
563
  targetAccess: "relay",
200
- secretBrokering: "native"
564
+ secretBrokering: "native",
565
+ egressControl: "hosts-and-cidrs",
566
+ persistence: sandboxDriverCatalog.daytona.metadata.persistence
201
567
  },
202
568
  ensureHost,
569
+ // Provider-state enumeration for `garbageCollect()`.
570
+ //
571
+ // Two label sets exist on a sandbox this driver created (`ensureHost`
572
+ // above): the manager's flat set — `app`, `workspaceId`, `epoch`,
573
+ // `homeRegion` (built at index.ts `ensureHostInput`) — and the dotted
574
+ // `claxedo.workspaceId` this driver adds for `findExisting`. Every target
575
+ // returned here carries `sandbox.labels` **as the provider reports them**,
576
+ // never labels reconstructed from ensure inputs: GC's ownership check reads
577
+ // `labels.app` and its identity check reads flat `labels.workspaceId` /
578
+ // `labels.epoch`, so a synthesized label set would decide the fate of a
579
+ // sandbox using values that never left this process. (That is the bug in
580
+ // the `exe` driver's `list()`, which sources labels from a per-process
581
+ // `Map` — after a restart it reports `labels: undefined` and GC skips
582
+ // every sandbox as unlabeled. Not fixed here; different owner.)
583
+ //
584
+ // The filter is deliberately WIDE: any claxedo ownership marker qualifies.
585
+ // Narrowing it here would re-create the defect W1 exists to remove — an
586
+ // orphan the sweep cannot see is an orphan that lives forever — and the
587
+ // authority on what may be DESTROYED is the manager's `app`-label check,
588
+ // which skips anything else as `unmanaged_app_label`. Visibility is this
589
+ // function's job; destruction is not.
590
+ //
591
+ // No preview URL is resolved: `getSignedPreviewUrl` is a per-sandbox
592
+ // round-trip and GC only needs identity, so a sweep over N orphans would
593
+ // cost N extra API calls to fill a field it then discards.
594
+ async list() {
595
+ const client = await resolveClient();
596
+ if (!client.list) {
597
+ throw new Error("daytona sandbox client does not support listing");
598
+ }
599
+ const targets = [];
600
+ const seen = /* @__PURE__ */ new Set();
601
+ for (let page = 1; page <= MAX_LIST_PAGES; page++) {
602
+ const result = await client.list(void 0, page, LIST_PAGE_SIZE);
603
+ const items = result?.items ?? [];
604
+ for (const sandbox of items) {
605
+ const labels = sandbox.labels;
606
+ const workspaceId = labels?.workspaceId ?? labels?.["claxedo.workspaceId"];
607
+ if (!workspaceId || seen.has(sandbox.id)) continue;
608
+ seen.add(sandbox.id);
609
+ targets.push({
610
+ workspaceId,
611
+ sandboxId: sandbox.id,
612
+ // Identity, not a reachable address — never routed through.
613
+ url: sandbox.id,
614
+ // Must equal what `ensureHost` stored on the lease, or a LIVE
615
+ // sandbox fails GC's identity check and gets destroyed. Both paths
616
+ // derive it from `labelName(workspaceId)`.
617
+ hostId: labelName(workspaceId),
618
+ driverResourceId: sandbox.id,
619
+ labels,
620
+ driver: { id: "daytona", resourceId: sandbox.id }
621
+ });
622
+ }
623
+ if (items.length < LIST_PAGE_SIZE) break;
624
+ }
625
+ return targets;
626
+ },
203
627
  async resumeHost(input) {
204
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
+ }
205
633
  if (!await ensureStarted(sandbox)) return { provisioning: true, retryAfterMs: 2e3 };
634
+ if (await applyNetworkPolicy(sandbox, input.ensure) === "retry") {
635
+ return { provisioning: true, retryAfterMs: 2e3 };
636
+ }
206
637
  return readyTarget(input.ensure, sandbox, input.ensure.hostId ?? labelName(input.ensure.workspaceId));
207
638
  },
208
639
  async touch(target) {
@@ -216,14 +647,33 @@ function createDaytonaSandboxDriver(options) {
216
647
  },
217
648
  async destroy(target) {
218
649
  await sandboxById(target.sandboxId).then((sandbox) => sandbox.delete(operationTimeout)).catch((err) => {
219
- const status = err.response?.status ?? err.status;
220
- if (status !== 404) throw err;
650
+ if (driverErrorSignals(err).status !== 404) throw err;
221
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
+ }
661
+ },
662
+ async snapshot(target) {
663
+ const sandbox = await sandboxById(target.sandboxId);
664
+ if (!sandbox._experimental_createSnapshot) {
665
+ throw new Error("Daytona sandbox client does not support filesystem snapshots");
666
+ }
667
+ const snapshotId = `claxedo-${target.workspaceId ?? target.sandboxId}-${Date.now()}`;
668
+ await sandbox._experimental_createSnapshot(snapshotId, operationTimeout);
669
+ return { snapshotId };
222
670
  }
223
671
  };
224
672
  }
225
673
  function network(input) {
226
674
  if (!input.net || input.net.mode === "allow-all") return {};
675
+ const hosts = input.net.hosts ?? [];
676
+ if (hosts.length > 0) return { domainAllowList: formatDaytonaDomainAllowList(hosts) };
227
677
  const cidrs = input.net.cidrs ?? [];
228
678
  if (cidrs.length === 0) return { networkBlockAll: true };
229
679
  return { networkAllowList: formatDaytonaAllowList(cidrs) };