@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.
@@ -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: "exe.dev",
35
- credentialFields: [{ key: "api_token", label: "API Token", secret: true }],
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: "Daytona",
56
- credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
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
- // Daytona is the only driver that can filter egress by NAME as well as
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: "Modal",
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
- // Modal can cut the network entirely (`blockNetwork`) but cannot express
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: "Vercel",
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
- // Vercel's sandbox firewall takes a hostname allow list (or "deny-all"),
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: "Cloudflare",
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: "proxy",
144
- // The Cloudflare Sandbox Worker exposes no egress filter: its broker
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: "Box",
164
- credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
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: "Docker",
185
- credentialFields: [{ key: "image", label: "Image" }],
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 res = await doFetch(url, {
260
- method,
261
- headers,
262
- ...method === "POST" ? { body: JSON.stringify(body) } : {},
263
- signal: AbortSignal.timeout(timeoutMs)
264
- });
265
- const data = await res.json().catch(() => ({}));
266
- return { status: res.status, data };
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
- const env = {
273
- ...workspaceRuntimeTargetEnv({
274
- workspaceId: input.workspaceId,
275
- hostId,
276
- directory: workspaceDirectory(input),
277
- port: runtimePort,
278
- host: "0.0.0.0"
279
- }),
280
- ...workspaceRuntimeSourceEnv({ source: input.source }),
281
- // Must match RUNTIME_DATA_DIR's capture set below — see its comment.
282
- CLAXEDO_DATA_DIR: RUNTIME_DATA_DIR,
283
- ...input.env,
284
- ...await options.env?.(input, { id: hostId })
285
- };
286
- if (options.runner) env.WORKSPACE_RUNTIME_RUNNER = options.runner;
287
- if (options.controlEnv?.relayJwksUrl) env.WORKSPACE_RUNTIME_RELAY_JWKS_URL = options.controlEnv.relayJwksUrl;
288
- if (options.controlEnv?.relayVerifyPem) env.WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM = options.controlEnv.relayVerifyPem;
289
- if (options.controlEnv?.managementJwksUrl) env.WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL = options.controlEnv.managementJwksUrl;
290
- return env;
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 (!input.secrets?.length) return void 0;
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 \u2014 the egress proxy injects the value as an HTTP 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
- return { hosts: secret.hosts, header: secret.header, value: secret.value };
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
- if (status >= 500) return { provisioning: true, retryAfterMs: 2e3 };
331
- if (status >= 400 || !data?.url) {
332
- throw new Error(`Cloudflare ensure-runtime failed (${status}): ${data?.error ?? "no runtime url"}`);
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: data.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 res = await doFetch(`${base}/sandboxes`, {
382
- method: "GET",
383
- headers,
384
- signal: AbortSignal.timeout(timeoutMs)
385
- });
386
- const data = await res.json().catch(() => ({}));
387
- if (res.status === 404 || res.status === 501 || data?.supported === false) {
388
- throw new CloudflareSandboxListingUnsupportedError(
389
- 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`
390
- );
391
- }
392
- if (!res.ok) {
393
- throw new Error(`Cloudflare sandbox listing failed (${res.status}): ${data?.error ?? "unknown error"}`);
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: "proxy",
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
- if (status >= 400 || !data.backupId) {
444
- throw new Error(`Cloudflare backup failed (${status}): ${data.error ?? "no backup id"}`);
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: data.backupId };
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
- * Create-or-update an org secret whose value is only substituted by
79
- * Daytona's egress proxy for requests to `hosts`
80
- * (https://www.daytona.io/docs/en/secrets/). Referenced from `create`'s
81
- * `secrets` map; inside the sandbox the env var holds only an opaque
82
- * `dtn_secret_…` placeholder, never the value.
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
- upsertSecret?: (params: {
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: string[];
88
- }) => Promise<void>;
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;