@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.
@@ -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: "exe.dev",
70
- credentialFields: [{ key: "api_token", label: "API Token", secret: true }],
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: "Daytona",
91
- credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
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
- // Daytona is the only driver that can filter egress by NAME as well as
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: "Modal",
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
- // Modal can cut the network entirely (`blockNetwork`) but cannot express
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: "Vercel",
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
- // Vercel's sandbox firewall takes a hostname allow list (or "deny-all"),
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: "Cloudflare",
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: "proxy",
179
- // The Cloudflare Sandbox Worker exposes no egress filter: its broker
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: "Box",
199
- credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
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: "Docker",
220
- credentialFields: [{ key: "image", label: "Image" }],
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
- const safe = (value) => value.replace(/[^a-zA-Z0-9_-]+/g, "-");
252
- return `claxedo-${safe(workspaceId)}-${safe(secretName)}`;
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
- const shaped = err;
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
- async upsertSecret({ name, value, hosts }) {
289
- await sdk.secret.create({ name, value, hosts });
290
- }
367
+ secret: sdk.secret
291
368
  };
292
369
  }
293
370
  function createDaytonaSandboxDriver(options) {
294
- 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
+ }
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
- const env = {
305
- ...workspaceRuntimeTargetEnv({
306
- workspaceId: input.workspaceId,
307
- hostId,
308
- directory,
309
- port: runtimePort
310
- }),
311
- ...workspaceRuntimeSourceEnv({ source: input.source }),
312
- ...input.env
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 client.get(sandboxId);
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 brokeredSecretReferences(input) {
370
- if (!input.secrets?.length) return {};
371
- if (!client.upsertSecret) {
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
- 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 ?? []) {
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 secretName = daytonaSecretName(input.workspaceId, secret.name);
380
- await client.upsertSecret({ name: secretName, value: secret.value, hosts: secret.hosts });
381
- 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);
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 sandbox = existing ?? await client.create({
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
- ...Object.keys(secrets).length ? { secrets } : {},
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
- const status = err.response?.status ?? err.status;
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);