@claxedo/sandbox-manager 0.5.2 → 0.7.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.
@@ -5,6 +5,9 @@ import { Daytona } from "@daytona/sdk";
5
5
  function formatDaytonaAllowList(cidrs) {
6
6
  return cidrs.slice(0, 10).join(",");
7
7
  }
8
+ function formatDaytonaDomainAllowList(hosts) {
9
+ return [...new Set(hosts)].join(",");
10
+ }
8
11
 
9
12
  // src/runtime-env.ts
10
13
  function workspaceRuntimeTargetEnv(input) {
@@ -33,11 +36,214 @@ function shell(input) {
33
36
  // src/constants.ts
34
37
  var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
35
38
 
39
+ // src/hosted-network-policy.ts
40
+ var SANDBOX_MODEL_PROVIDER_HOSTS = [
41
+ "api.anthropic.com",
42
+ "api.openai.com",
43
+ "generativelanguage.googleapis.com",
44
+ "openrouter.ai",
45
+ // Model catalog + engine config, fetched on runner startup.
46
+ "models.dev",
47
+ "opencode.ai"
48
+ ];
49
+ var SANDBOX_PACKAGE_REGISTRY_HOSTS = [
50
+ "registry.npmjs.org",
51
+ "nodejs.org",
52
+ "pypi.org",
53
+ "files.pythonhosted.org",
54
+ "crates.io",
55
+ "static.crates.io",
56
+ "index.crates.io",
57
+ "proxy.golang.org",
58
+ "sum.golang.org"
59
+ ];
60
+ var SANDBOX_BASELINE_EGRESS_HOSTS = [
61
+ ...SANDBOX_MODEL_PROVIDER_HOSTS,
62
+ ...SANDBOX_PACKAGE_REGISTRY_HOSTS
63
+ ];
64
+
65
+ // src/driver-catalog.ts
66
+ var sandboxDriverCatalog = {
67
+ exe: {
68
+ id: "exe",
69
+ label: "exe.dev",
70
+ credentialFields: [{ key: "api_token", label: "API Token", secret: true }],
71
+ metadata: {
72
+ driverRunsIn: ["worker", "node"],
73
+ hostStopBehavior: "not-supported",
74
+ hostResumeBehavior: "same-host",
75
+ targetAccess: "relay",
76
+ secretBrokering: "none",
77
+ egressControl: "none",
78
+ persistence: {
79
+ resume: "same-sandbox",
80
+ capture: "none",
81
+ clone: true,
82
+ captureSource: "not-applicable",
83
+ retention: "provider-managed",
84
+ restoreMount: "same-resource"
85
+ }
86
+ }
87
+ },
88
+ daytona: {
89
+ id: "daytona",
90
+ label: "Daytona",
91
+ credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
92
+ metadata: {
93
+ driverRunsIn: ["worker", "node"],
94
+ hostStopBehavior: "suspends-host",
95
+ hostResumeBehavior: "same-host",
96
+ targetAccess: "relay",
97
+ 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.
101
+ egressControl: "hosts-and-cidrs",
102
+ persistence: {
103
+ resume: "same-sandbox",
104
+ capture: "filesystem",
105
+ clone: false,
106
+ captureSource: "preserved",
107
+ retention: "provider-managed",
108
+ restoreMount: "new-resource"
109
+ }
110
+ }
111
+ },
112
+ modal: {
113
+ 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
+ ],
119
+ metadata: {
120
+ driverRunsIn: ["node"],
121
+ hostStopBehavior: "terminates-host",
122
+ hostResumeBehavior: "replacement-host",
123
+ targetAccess: "relay",
124
+ 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.
128
+ egressControl: "none",
129
+ persistence: {
130
+ resume: "replacement-restore",
131
+ capture: "filesystem",
132
+ clone: false,
133
+ captureSource: "preserved",
134
+ retention: "provider-managed",
135
+ restoreMount: "new-resource"
136
+ }
137
+ }
138
+ },
139
+ vercel: {
140
+ 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
+ ],
147
+ metadata: {
148
+ driverRunsIn: ["node"],
149
+ hostStopBehavior: "terminates-host",
150
+ hostResumeBehavior: "replacement-host",
151
+ targetAccess: "relay",
152
+ secretBrokering: "native",
153
+ // Vercel's sandbox firewall takes a hostname allow list (or "deny-all"),
154
+ // never CIDRs.
155
+ egressControl: "hosts",
156
+ persistence: {
157
+ resume: "replacement-restore",
158
+ capture: "filesystem",
159
+ clone: false,
160
+ captureSource: "stopped",
161
+ retention: "explicit",
162
+ restoreMount: "new-resource"
163
+ }
164
+ }
165
+ },
166
+ cloudflare: {
167
+ id: "cloudflare",
168
+ label: "Cloudflare",
169
+ credentialFields: [
170
+ { key: "api_token", label: "API Token", secret: true },
171
+ { key: "worker_url", label: "Worker URL" }
172
+ ],
173
+ metadata: {
174
+ driverRunsIn: ["worker"],
175
+ hostStopBehavior: "not-supported",
176
+ hostResumeBehavior: "same-host",
177
+ 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.
185
+ egressControl: "none",
186
+ persistence: {
187
+ resume: "replacement-restore",
188
+ capture: "directories",
189
+ clone: false,
190
+ captureSource: "preserved",
191
+ retention: "provider-managed",
192
+ restoreMount: "copy-on-write"
193
+ }
194
+ }
195
+ },
196
+ box: {
197
+ id: "box",
198
+ label: "Box",
199
+ credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
200
+ metadata: {
201
+ driverRunsIn: ["node"],
202
+ hostStopBehavior: "suspends-host",
203
+ hostResumeBehavior: "same-host",
204
+ targetAccess: "relay",
205
+ secretBrokering: "none",
206
+ egressControl: "none",
207
+ persistence: {
208
+ resume: "same-sandbox",
209
+ capture: "none",
210
+ clone: false,
211
+ captureSource: "not-applicable",
212
+ retention: "provider-managed",
213
+ restoreMount: "same-resource"
214
+ }
215
+ }
216
+ },
217
+ docker: {
218
+ id: "docker",
219
+ label: "Docker",
220
+ credentialFields: [{ key: "image", label: "Image" }],
221
+ metadata: {
222
+ driverRunsIn: ["local"],
223
+ hostStopBehavior: "terminates-host",
224
+ hostResumeBehavior: "same-host",
225
+ targetAccess: "loopback",
226
+ secretBrokering: "none",
227
+ egressControl: "none",
228
+ persistence: {
229
+ resume: "replacement-restore",
230
+ capture: "same-resource",
231
+ clone: false,
232
+ captureSource: "preserved",
233
+ retention: "provider-managed",
234
+ restoreMount: "same-resource"
235
+ }
236
+ }
237
+ }
238
+ };
239
+
36
240
  // src/drivers/daytona.ts
37
241
  var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
38
242
  var DEFAULT_WORKSPACE_DIR = "/workspace";
39
243
  var DEFAULT_PREVIEW_EXPIRY_S = 3600;
40
244
  var DEFAULT_OPERATION_TIMEOUT_S = 60;
245
+ var LIST_PAGE_SIZE = 100;
246
+ var MAX_LIST_PAGES = 100;
41
247
  function labelName(workspaceId) {
42
248
  return `claxedo-${workspaceId}`;
43
249
  }
@@ -65,6 +271,15 @@ function createDefaultClient(options) {
65
271
  for await (const sandbox of sdk.list({ labels })) return sandbox;
66
272
  return void 0;
67
273
  },
274
+ // Page 1 carries everything; later pages are empty. See DaytonaClientLike.list.
275
+ async list(labels, page = 1, limit) {
276
+ if (page > 1) return { items: [] };
277
+ const items = [];
278
+ for await (const sandbox of sdk.list({ ...labels ? { labels } : {}, ...limit ? { limit } : {} })) {
279
+ items.push(sandbox);
280
+ }
281
+ return { items };
282
+ },
68
283
  create(params, operation) {
69
284
  if (params.image) return sdk.create({ ...params, image: params.image }, operation);
70
285
  return sdk.create(params, operation);
@@ -167,12 +382,29 @@ function createDaytonaSandboxDriver(options) {
167
382
  }
168
383
  return references;
169
384
  }
385
+ async function applyNetworkPolicy(sandbox, input) {
386
+ const net = network(input);
387
+ if (Object.keys(net).length === 0) return "applied";
388
+ if (!sandbox.updateNetworkSettings) {
389
+ throw new Error(
390
+ `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`
391
+ );
392
+ }
393
+ try {
394
+ await sandbox.updateNetworkSettings(net);
395
+ } catch (err) {
396
+ if (transientDriverError(err)) return "retry";
397
+ throw err;
398
+ }
399
+ return "applied";
400
+ }
170
401
  async function ensureHost(input) {
171
402
  const hostId = labelName(input.workspaceId);
172
403
  const bootSource = input.bootSource?.kind === "image" ? { image: input.bootSource.image } : { snapshot: input.bootSource?.kind === "driver-snapshot" ? input.bootSource.snapshotId : input.snapshot ?? options.baseSnapshot };
173
404
  const net = network(input);
174
405
  const secrets = await brokeredSecretReferences(input);
175
- const sandbox = await findExisting(input.workspaceId) ?? await client.create({
406
+ const existing = await findExisting(input.workspaceId);
407
+ const sandbox = existing ?? await client.create({
176
408
  name: labelName(input.workspaceId),
177
409
  ...bootSource,
178
410
  envVars: staticBootEnv(input, hostId, workspaceDirectory(input)),
@@ -188,6 +420,9 @@ function createDaytonaSandboxDriver(options) {
188
420
  });
189
421
  if (!sandbox) return { provisioning: true, retryAfterMs: 2e3 };
190
422
  if (!await ensureStarted(sandbox)) return { provisioning: true, retryAfterMs: 2e3 };
423
+ if (existing && await applyNetworkPolicy(existing, input) === "retry") {
424
+ return { provisioning: true, retryAfterMs: 2e3 };
425
+ }
191
426
  return readyTarget(input, sandbox, hostId);
192
427
  }
193
428
  return {
@@ -197,12 +432,74 @@ function createDaytonaSandboxDriver(options) {
197
432
  hostStopBehavior: "suspends-host",
198
433
  hostResumeBehavior: "same-host",
199
434
  targetAccess: "relay",
200
- secretBrokering: "native"
435
+ secretBrokering: "native",
436
+ egressControl: "hosts-and-cidrs",
437
+ persistence: sandboxDriverCatalog.daytona.metadata.persistence
201
438
  },
202
439
  ensureHost,
440
+ // Provider-state enumeration for `garbageCollect()`.
441
+ //
442
+ // Two label sets exist on a sandbox this driver created (`ensureHost`
443
+ // above): the manager's flat set — `app`, `workspaceId`, `epoch`,
444
+ // `homeRegion` (built at index.ts `ensureHostInput`) — and the dotted
445
+ // `claxedo.workspaceId` this driver adds for `findExisting`. Every target
446
+ // returned here carries `sandbox.labels` **as the provider reports them**,
447
+ // never labels reconstructed from ensure inputs: GC's ownership check reads
448
+ // `labels.app` and its identity check reads flat `labels.workspaceId` /
449
+ // `labels.epoch`, so a synthesized label set would decide the fate of a
450
+ // sandbox using values that never left this process. (That is the bug in
451
+ // the `exe` driver's `list()`, which sources labels from a per-process
452
+ // `Map` — after a restart it reports `labels: undefined` and GC skips
453
+ // every sandbox as unlabeled. Not fixed here; different owner.)
454
+ //
455
+ // The filter is deliberately WIDE: any claxedo ownership marker qualifies.
456
+ // Narrowing it here would re-create the defect W1 exists to remove — an
457
+ // orphan the sweep cannot see is an orphan that lives forever — and the
458
+ // authority on what may be DESTROYED is the manager's `app`-label check,
459
+ // which skips anything else as `unmanaged_app_label`. Visibility is this
460
+ // function's job; destruction is not.
461
+ //
462
+ // No preview URL is resolved: `getSignedPreviewUrl` is a per-sandbox
463
+ // round-trip and GC only needs identity, so a sweep over N orphans would
464
+ // cost N extra API calls to fill a field it then discards.
465
+ async list() {
466
+ if (!client.list) {
467
+ throw new Error("daytona sandbox client does not support listing");
468
+ }
469
+ const targets = [];
470
+ const seen = /* @__PURE__ */ new Set();
471
+ for (let page = 1; page <= MAX_LIST_PAGES; page++) {
472
+ const result = await client.list(void 0, page, LIST_PAGE_SIZE);
473
+ const items = result?.items ?? [];
474
+ for (const sandbox of items) {
475
+ const labels = sandbox.labels;
476
+ const workspaceId = labels?.workspaceId ?? labels?.["claxedo.workspaceId"];
477
+ if (!workspaceId || seen.has(sandbox.id)) continue;
478
+ seen.add(sandbox.id);
479
+ targets.push({
480
+ workspaceId,
481
+ sandboxId: sandbox.id,
482
+ // Identity, not a reachable address — never routed through.
483
+ url: sandbox.id,
484
+ // Must equal what `ensureHost` stored on the lease, or a LIVE
485
+ // sandbox fails GC's identity check and gets destroyed. Both paths
486
+ // derive it from `labelName(workspaceId)`.
487
+ hostId: labelName(workspaceId),
488
+ driverResourceId: sandbox.id,
489
+ labels,
490
+ driver: { id: "daytona", resourceId: sandbox.id }
491
+ });
492
+ }
493
+ if (items.length < LIST_PAGE_SIZE) break;
494
+ }
495
+ return targets;
496
+ },
203
497
  async resumeHost(input) {
204
498
  const sandbox = await sandboxById(input.lease.sandboxId);
205
499
  if (!await ensureStarted(sandbox)) return { provisioning: true, retryAfterMs: 2e3 };
500
+ if (await applyNetworkPolicy(sandbox, input.ensure) === "retry") {
501
+ return { provisioning: true, retryAfterMs: 2e3 };
502
+ }
206
503
  return readyTarget(input.ensure, sandbox, input.ensure.hostId ?? labelName(input.ensure.workspaceId));
207
504
  },
208
505
  async touch(target) {
@@ -219,11 +516,22 @@ function createDaytonaSandboxDriver(options) {
219
516
  const status = err.response?.status ?? err.status;
220
517
  if (status !== 404) throw err;
221
518
  });
519
+ },
520
+ async snapshot(target) {
521
+ const sandbox = await sandboxById(target.sandboxId);
522
+ if (!sandbox._experimental_createSnapshot) {
523
+ throw new Error("Daytona sandbox client does not support filesystem snapshots");
524
+ }
525
+ const snapshotId = `claxedo-${target.workspaceId ?? target.sandboxId}-${Date.now()}`;
526
+ await sandbox._experimental_createSnapshot(snapshotId, operationTimeout);
527
+ return { snapshotId };
222
528
  }
223
529
  };
224
530
  }
225
531
  function network(input) {
226
532
  if (!input.net || input.net.mode === "allow-all") return {};
533
+ const hosts = input.net.hosts ?? [];
534
+ if (hosts.length > 0) return { domainAllowList: formatDaytonaDomainAllowList(hosts) };
227
535
  const cidrs = input.net.cidrs ?? [];
228
536
  if (cidrs.length === 0) return { networkBlockAll: true };
229
537
  return { networkAllowList: formatDaytonaAllowList(cidrs) };
@@ -37,16 +37,8 @@ import { Image, DaytonaNotFoundError } from "@daytona/sdk";
37
37
 
38
38
  // src/runtime-version.ts
39
39
  var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
40
- var versionCache;
41
40
  function workspaceRuntimeVersion() {
42
- if (versionCache) return versionCache;
43
- const envVersion = process.env.WORKSPACE_RUNTIME_VERSION;
44
- if (envVersion) {
45
- versionCache = envVersion;
46
- return versionCache;
47
- }
48
- versionCache = DEFAULT_WORKSPACE_RUNTIME_VERSION;
49
- return versionCache;
41
+ return process.env.WORKSPACE_RUNTIME_VERSION || DEFAULT_WORKSPACE_RUNTIME_VERSION;
50
42
  }
51
43
 
52
44
  // src/image-name.ts
@@ -75,6 +67,207 @@ var SANDBOX_IMAGE_REPOSITORY = sandboxImageRepository();
75
67
  var SANDBOX_IMAGE = process.env.CLAXEDO_SANDBOX_IMAGE || defaultSandboxImage();
76
68
  var SNAPSHOT_NAME = process.env.CLAXEDO_SNAPSHOT_NAME || defaultSnapshotName();
77
69
 
70
+ // src/hosted-network-policy.ts
71
+ var SANDBOX_MODEL_PROVIDER_HOSTS = [
72
+ "api.anthropic.com",
73
+ "api.openai.com",
74
+ "generativelanguage.googleapis.com",
75
+ "openrouter.ai",
76
+ // Model catalog + engine config, fetched on runner startup.
77
+ "models.dev",
78
+ "opencode.ai"
79
+ ];
80
+ var SANDBOX_PACKAGE_REGISTRY_HOSTS = [
81
+ "registry.npmjs.org",
82
+ "nodejs.org",
83
+ "pypi.org",
84
+ "files.pythonhosted.org",
85
+ "crates.io",
86
+ "static.crates.io",
87
+ "index.crates.io",
88
+ "proxy.golang.org",
89
+ "sum.golang.org"
90
+ ];
91
+ var SANDBOX_BASELINE_EGRESS_HOSTS = [
92
+ ...SANDBOX_MODEL_PROVIDER_HOSTS,
93
+ ...SANDBOX_PACKAGE_REGISTRY_HOSTS
94
+ ];
95
+
96
+ // src/driver-catalog.ts
97
+ var sandboxDriverCatalog = {
98
+ exe: {
99
+ id: "exe",
100
+ label: "exe.dev",
101
+ credentialFields: [{ key: "api_token", label: "API Token", secret: true }],
102
+ metadata: {
103
+ driverRunsIn: ["worker", "node"],
104
+ hostStopBehavior: "not-supported",
105
+ hostResumeBehavior: "same-host",
106
+ targetAccess: "relay",
107
+ secretBrokering: "none",
108
+ egressControl: "none",
109
+ persistence: {
110
+ resume: "same-sandbox",
111
+ capture: "none",
112
+ clone: true,
113
+ captureSource: "not-applicable",
114
+ retention: "provider-managed",
115
+ restoreMount: "same-resource"
116
+ }
117
+ }
118
+ },
119
+ daytona: {
120
+ id: "daytona",
121
+ label: "Daytona",
122
+ credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
123
+ metadata: {
124
+ driverRunsIn: ["worker", "node"],
125
+ hostStopBehavior: "suspends-host",
126
+ hostResumeBehavior: "same-host",
127
+ targetAccess: "relay",
128
+ secretBrokering: "native",
129
+ // Daytona is the only driver that can filter egress by NAME as well as
130
+ // by address: `domainAllowList` (names) alongside `networkAllowList`
131
+ // (CIDRs), with `networkBlockAll` as the deny-all floor.
132
+ egressControl: "hosts-and-cidrs",
133
+ persistence: {
134
+ resume: "same-sandbox",
135
+ capture: "filesystem",
136
+ clone: false,
137
+ captureSource: "preserved",
138
+ retention: "provider-managed",
139
+ restoreMount: "new-resource"
140
+ }
141
+ }
142
+ },
143
+ modal: {
144
+ id: "modal",
145
+ label: "Modal",
146
+ credentialFields: [
147
+ { key: "token_id", label: "Token ID", secret: true },
148
+ { key: "token_secret", label: "Token Secret", secret: true }
149
+ ],
150
+ metadata: {
151
+ driverRunsIn: ["node"],
152
+ hostStopBehavior: "terminates-host",
153
+ hostResumeBehavior: "replacement-host",
154
+ targetAccess: "relay",
155
+ secretBrokering: "none",
156
+ // Modal can cut the network entirely (`blockNetwork`) but cannot express
157
+ // an allowlist, and its driver throws on a host policy. A blackout is
158
+ // not containment for a workspace that has to clone and reach a model.
159
+ egressControl: "none",
160
+ persistence: {
161
+ resume: "replacement-restore",
162
+ capture: "filesystem",
163
+ clone: false,
164
+ captureSource: "preserved",
165
+ retention: "provider-managed",
166
+ restoreMount: "new-resource"
167
+ }
168
+ }
169
+ },
170
+ vercel: {
171
+ id: "vercel",
172
+ label: "Vercel",
173
+ credentialFields: [
174
+ { key: "access_token", label: "Access Token", secret: true },
175
+ { key: "team_id", label: "Team ID" },
176
+ { key: "project_id", label: "Project ID" }
177
+ ],
178
+ metadata: {
179
+ driverRunsIn: ["node"],
180
+ hostStopBehavior: "terminates-host",
181
+ hostResumeBehavior: "replacement-host",
182
+ targetAccess: "relay",
183
+ secretBrokering: "native",
184
+ // Vercel's sandbox firewall takes a hostname allow list (or "deny-all"),
185
+ // never CIDRs.
186
+ egressControl: "hosts",
187
+ persistence: {
188
+ resume: "replacement-restore",
189
+ capture: "filesystem",
190
+ clone: false,
191
+ captureSource: "stopped",
192
+ retention: "explicit",
193
+ restoreMount: "new-resource"
194
+ }
195
+ }
196
+ },
197
+ cloudflare: {
198
+ id: "cloudflare",
199
+ label: "Cloudflare",
200
+ credentialFields: [
201
+ { key: "api_token", label: "API Token", secret: true },
202
+ { key: "worker_url", label: "Worker URL" }
203
+ ],
204
+ metadata: {
205
+ driverRunsIn: ["worker"],
206
+ hostStopBehavior: "not-supported",
207
+ hostResumeBehavior: "same-host",
208
+ targetAccess: "relay",
209
+ secretBrokering: "proxy",
210
+ // The Cloudflare Sandbox Worker exposes no egress filter: its broker
211
+ // (drivers/cloudflare-egress.ts) is an OPT-IN proxy the sandbox chooses
212
+ // to route brokered-credential requests through, not a boundary. The
213
+ // driver therefore drops `net` entirely — which is exactly why this
214
+ // must be declared, so the manager refuses instead of provisioning a
215
+ // sandbox the caller believes is contained.
216
+ egressControl: "none",
217
+ persistence: {
218
+ resume: "replacement-restore",
219
+ capture: "directories",
220
+ clone: false,
221
+ captureSource: "preserved",
222
+ retention: "provider-managed",
223
+ restoreMount: "copy-on-write"
224
+ }
225
+ }
226
+ },
227
+ box: {
228
+ id: "box",
229
+ label: "Box",
230
+ credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
231
+ metadata: {
232
+ driverRunsIn: ["node"],
233
+ hostStopBehavior: "suspends-host",
234
+ hostResumeBehavior: "same-host",
235
+ targetAccess: "relay",
236
+ secretBrokering: "none",
237
+ egressControl: "none",
238
+ persistence: {
239
+ resume: "same-sandbox",
240
+ capture: "none",
241
+ clone: false,
242
+ captureSource: "not-applicable",
243
+ retention: "provider-managed",
244
+ restoreMount: "same-resource"
245
+ }
246
+ }
247
+ },
248
+ docker: {
249
+ id: "docker",
250
+ label: "Docker",
251
+ credentialFields: [{ key: "image", label: "Image" }],
252
+ metadata: {
253
+ driverRunsIn: ["local"],
254
+ hostStopBehavior: "terminates-host",
255
+ hostResumeBehavior: "same-host",
256
+ targetAccess: "loopback",
257
+ secretBrokering: "none",
258
+ egressControl: "none",
259
+ persistence: {
260
+ resume: "replacement-restore",
261
+ capture: "same-resource",
262
+ clone: false,
263
+ captureSource: "preserved",
264
+ retention: "provider-managed",
265
+ restoreMount: "same-resource"
266
+ }
267
+ }
268
+ }
269
+ };
270
+
78
271
  // src/drivers/docker.ts
79
272
  var execFileAsync = promisify(execFile);
80
273
  var DEFAULT_OPERATION_TIMEOUT_MS = 6e4;
@@ -326,7 +519,9 @@ function createDockerSandboxDriver(options) {
326
519
  hostStopBehavior: "terminates-host",
327
520
  hostResumeBehavior: "same-host",
328
521
  targetAccess: "loopback",
329
- secretBrokering: "none"
522
+ secretBrokering: "none",
523
+ egressControl: "none",
524
+ persistence: sandboxDriverCatalog.docker.metadata.persistence
330
525
  },
331
526
  ensureHost,
332
527
  resumeHost,
@@ -0,0 +1,26 @@
1
+ import type { SandboxDriver, SandboxDriverEnsureInput } from "..";
2
+ type ExeVm = {
3
+ vm_name: string;
4
+ https_url?: string;
5
+ ssh_dest?: string;
6
+ status?: string;
7
+ /** Provider-side tags, written at create — the durable half of our labels. */
8
+ tags?: string[];
9
+ };
10
+ export type ExeSandboxDriverOptions = {
11
+ apiToken: string;
12
+ endpoint?: string;
13
+ image?: string;
14
+ runtimeCommand?: string;
15
+ workspaceDir?: string;
16
+ runtimePort?: number;
17
+ runner?: string;
18
+ operationTimeoutMs?: number;
19
+ healthTimeoutMs?: number;
20
+ healthIntervalMs?: number;
21
+ fetchImpl?: typeof fetch;
22
+ env?: (input: SandboxDriverEnsureInput, vm: ExeVm) => Record<string, string> | Promise<Record<string, string>>;
23
+ };
24
+ export declare function exeWorkspaceName(workspaceId: string, epoch: number): string;
25
+ export declare function createExeSandboxDriver(options: ExeSandboxDriverOptions): SandboxDriver;
26
+ export {};