@claxedo/sandbox-manager 0.7.0 → 0.10.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 (70) hide show
  1. package/README.md +245 -42
  2. package/dist/checkpoint-manager.d.ts +29 -4
  3. package/dist/checkpoint-manager.mjs +133 -24
  4. package/dist/command.d.ts +9 -2
  5. package/dist/command.mjs +8 -8
  6. package/dist/contract.d.ts +687 -0
  7. package/dist/driver-billing.d.ts +29 -0
  8. package/dist/driver-catalog.d.ts +43 -58
  9. package/dist/driver-catalog.mjs +192 -229
  10. package/dist/drivers/boat-client.d.ts +95 -0
  11. package/dist/drivers/boat-named-snapshots.d.ts +31 -0
  12. package/dist/drivers/boat-template.d.ts +49 -0
  13. package/dist/drivers/boat-template.mjs +1196 -0
  14. package/dist/drivers/boat.d.ts +64 -0
  15. package/dist/drivers/boat.mjs +990 -0
  16. package/dist/drivers/cloudflare.d.ts +5 -8
  17. package/dist/drivers/cloudflare.mjs +364 -213
  18. package/dist/drivers/docker.d.ts +4 -3
  19. package/dist/drivers/docker.mjs +244 -165
  20. package/dist/drivers/local-brokering.d.ts +14 -0
  21. package/dist/drivers/local-brokering.mjs +1029 -0
  22. package/dist/drivers/modal.d.ts +4 -7
  23. package/dist/drivers/modal.mjs +220 -155
  24. package/dist/drivers/transient-error.d.ts +13 -0
  25. package/dist/drivers/vercel.d.ts +21 -23
  26. package/dist/drivers/vercel.mjs +279 -169
  27. package/dist/egress-policy.d.ts +134 -0
  28. package/dist/egress-warnings.d.ts +22 -0
  29. package/dist/garbage-collect.d.ts +14 -0
  30. package/dist/hosted-network-policy.d.ts +11 -4
  31. package/dist/hosted-network-policy.mjs +221 -4
  32. package/dist/image-name.d.ts +12 -2
  33. package/dist/image-name.mjs +43 -0
  34. package/dist/image.d.ts +1 -20
  35. package/dist/image.mjs +11 -66
  36. package/dist/index.d.ts +8 -533
  37. package/dist/index.mjs +1420 -315
  38. package/dist/json.d.ts +13 -0
  39. package/dist/lease-failure.d.ts +173 -0
  40. package/dist/lease-lifecycle.d.ts +65 -0
  41. package/dist/lease-retry-wait.d.ts +8 -0
  42. package/dist/lease-start.d.ts +21 -0
  43. package/dist/lease-types.d.ts +15 -5
  44. package/dist/manager.d.ts +2 -0
  45. package/dist/runtime-env.d.ts +15 -2
  46. package/dist/runtime-env.mjs +50 -1
  47. package/dist/runtime-snapshot.d.ts +49 -0
  48. package/dist/runtime-version.d.ts +9 -0
  49. package/dist/runtime-version.mjs +2 -0
  50. package/dist/service-url-exposure.d.ts +1 -3
  51. package/dist/service-url-exposure.mjs +1 -26
  52. package/dist/start-fresh.d.ts +17 -0
  53. package/dist/start-telemetry.d.ts +37 -0
  54. package/dist/stores/memory.d.ts +1 -1
  55. package/dist/stores/memory.mjs +65 -54
  56. package/package.json +67 -81
  57. package/dist/daytona-allow-list.d.ts +0 -16
  58. package/dist/daytona-allow-list.mjs +0 -11
  59. package/dist/drivers/box.d.ts +0 -52
  60. package/dist/drivers/box.mjs +0 -499
  61. package/dist/drivers/cloudflare-egress.d.ts +0 -40
  62. package/dist/drivers/cloudflare-egress.mjs +0 -113
  63. package/dist/drivers/daytona.d.ts +0 -127
  64. package/dist/drivers/daytona.mjs +0 -541
  65. package/dist/drivers/exe.d.ts +0 -26
  66. package/dist/drivers/exe.mjs +0 -469
  67. package/dist/drivers/fetch-bridge.d.ts +0 -10
  68. package/dist/drivers/fetch-bridge.mjs +0 -111
  69. package/dist/lease-policy.d.ts +0 -48
  70. package/dist/lease-policy.mjs +0 -280
@@ -1,4 +1,5 @@
1
- import type { SandboxDriver, SandboxDriverEnsureInput } from "..";
1
+ import type { SandboxDriver, SandboxDriverEnsureInput } from "../contract";
2
+ import { type WorkspaceRuntimeControlEnv } from "../runtime-env";
2
3
  type ModalAppLike = unknown;
3
4
  type ModalImageLike = {
4
5
  imageId?: string;
@@ -48,12 +49,8 @@ export type ModalSandboxDriverOptions = {
48
49
  runtimePort?: number;
49
50
  runtimeCommand?: string;
50
51
  workspaceDir?: string;
51
- runner?: string;
52
- controlEnv?: {
53
- relayJwksUrl?: string;
54
- relayVerifyPem?: string;
55
- managementJwksUrl?: string;
56
- };
52
+ nativeHarness?: string;
53
+ controlEnv?: WorkspaceRuntimeControlEnv;
57
54
  env?: (input: SandboxDriverEnsureInput, host: {
58
55
  id: string;
59
56
  }) => Record<string, string> | Promise<Record<string, string>>;
@@ -1,7 +1,47 @@
1
- // src/drivers/modal.ts
2
- import { ModalClient, Probe } from "modal";
3
-
4
1
  // src/runtime-env.ts
2
+ var WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS = [
3
+ "WORKSPACE_RUNTIME_WORKSPACE_ID",
4
+ "WORKSPACE_RUNTIME_HOST_ID",
5
+ "WORKSPACE_RUNTIME_RELAY_WORKSPACE_IDS"
6
+ ];
7
+ function workspaceRuntimeIdentityEnvConflicts(env) {
8
+ if (!env) return [];
9
+ return WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS.filter((key) => env[key] !== void 0);
10
+ }
11
+ function assertWorkspaceRuntimeIdentityEnv(env) {
12
+ const conflicts = workspaceRuntimeIdentityEnvConflicts(env);
13
+ if (conflicts.length === 0) return;
14
+ throw new Error(
15
+ `sandbox env cannot set runtime identity ${conflicts.join(", ")}: the driver's placement owns the identity its target reports and the lease records`
16
+ );
17
+ }
18
+ var WORKSPACE_RUNTIME_PUBLIC_ENV_KEYS = /* @__PURE__ */ new Set([
19
+ ...WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS,
20
+ "WORKSPACE_RUNTIME_DIRECTORY",
21
+ "WORKSPACE_RUNTIME_PORT",
22
+ "WORKSPACE_RUNTIME_HOST",
23
+ "WORKSPACE_RUNTIME_SOURCE_KIND",
24
+ "WORKSPACE_RUNTIME_GIT_REPO_URL",
25
+ "WORKSPACE_RUNTIME_GIT_BRANCH",
26
+ "WORKSPACE_RUNTIME_NATIVE_HARNESS",
27
+ "WORKSPACE_RUNTIME_RELAY_JWKS_URL",
28
+ "WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL",
29
+ "WORKSPACE_RUNTIME_SESSION_AUTHORITY_URL"
30
+ ]);
31
+ function workspaceRuntimeBootEnv(input) {
32
+ assertWorkspaceRuntimeIdentityEnv(input.env);
33
+ const env = {
34
+ ...workspaceRuntimeTargetEnv(input),
35
+ ...workspaceRuntimeSourceEnv(input),
36
+ ...input.env
37
+ };
38
+ if (input.nativeHarness) env.WORKSPACE_RUNTIME_NATIVE_HARNESS = input.nativeHarness;
39
+ if (input.controlEnv?.relayJwksUrl) env.WORKSPACE_RUNTIME_RELAY_JWKS_URL = input.controlEnv.relayJwksUrl;
40
+ if (input.controlEnv?.relayVerifyPem) env.WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM = input.controlEnv.relayVerifyPem;
41
+ if (input.controlEnv?.managementJwksUrl) env.WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL = input.controlEnv.managementJwksUrl;
42
+ if (input.controlEnv?.sessionAuthorityUrl) env.WORKSPACE_RUNTIME_SESSION_AUTHORITY_URL = input.controlEnv.sessionAuthorityUrl;
43
+ return env;
44
+ }
5
45
  function workspaceRuntimeTargetEnv(input) {
6
46
  return {
7
47
  WORKSPACE_RUNTIME_WORKSPACE_ID: input.workspaceId,
@@ -28,9 +68,6 @@ function shell(input) {
28
68
  // src/constants.ts
29
69
  var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
30
70
 
31
- // src/image.ts
32
- import { Image, DaytonaNotFoundError } from "@daytona/sdk";
33
-
34
71
  // src/runtime-version.ts
35
72
  var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
36
73
  function workspaceRuntimeVersion() {
@@ -51,107 +88,87 @@ function buildIdSuffix(buildId, env) {
51
88
  const id = (buildId ?? env.CLAXEDO_SANDBOX_BUILD_ID)?.trim();
52
89
  return id ? `-${snapshotVersion(id)}` : "";
53
90
  }
54
- function defaultSandboxImage(version = workspaceRuntimeVersion(), buildId, env = process.env) {
55
- return `${sandboxImageRepository(env)}:workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}-v${SNAPSHOT_SCHEMA_VERSION}`;
56
- }
57
- function defaultSnapshotName(version = workspaceRuntimeVersion(), buildId, env = process.env) {
58
- return `claxedo-workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}-v${SNAPSHOT_SCHEMA_VERSION}`;
91
+ var AGENT_PLUGINS_TAG = "-agent-plugins";
92
+ function defaultSandboxImage(version = workspaceRuntimeVersion(), buildId, env = process.env, options = {}) {
93
+ const feature = options.agentPlugins ? AGENT_PLUGINS_TAG : "";
94
+ return `${sandboxImageRepository(env)}:workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}${feature}-v${SNAPSHOT_SCHEMA_VERSION}`;
59
95
  }
60
96
 
61
97
  // src/image.ts
62
98
  var SANDBOX_IMAGE_REPOSITORY = sandboxImageRepository();
63
99
  var SANDBOX_IMAGE = process.env.CLAXEDO_SANDBOX_IMAGE || defaultSandboxImage();
64
- var SNAPSHOT_NAME = process.env.CLAXEDO_SNAPSHOT_NAME || defaultSnapshotName();
65
100
 
66
- // src/hosted-network-policy.ts
67
- var SANDBOX_MODEL_PROVIDER_HOSTS = [
68
- "api.anthropic.com",
69
- "api.openai.com",
70
- "generativelanguage.googleapis.com",
71
- "openrouter.ai",
72
- // Model catalog + engine config, fetched on runner startup.
73
- "models.dev",
74
- "opencode.ai"
75
- ];
76
- var SANDBOX_PACKAGE_REGISTRY_HOSTS = [
77
- "registry.npmjs.org",
78
- "nodejs.org",
79
- "pypi.org",
80
- "files.pythonhosted.org",
81
- "crates.io",
82
- "static.crates.io",
83
- "index.crates.io",
84
- "proxy.golang.org",
85
- "sum.golang.org"
86
- ];
87
- var SANDBOX_BASELINE_EGRESS_HOSTS = [
88
- ...SANDBOX_MODEL_PROVIDER_HOSTS,
89
- ...SANDBOX_PACKAGE_REGISTRY_HOSTS
90
- ];
101
+ // src/driver-billing.ts
102
+ var HOUR_SECONDS = 3600;
103
+ var boatBilling = {
104
+ source: { url: "https://docs.boat.dev/pricing", readOn: "2026-10-12" },
105
+ meter: "Boat bills machine time per second while a sandbox runs, at an hourly rate per size; a stopped sandbox costs nothing.",
106
+ rates: {
107
+ small: { size: "small", vcpu: 2, memoryGb: 4, usdPerHour: 0.018 },
108
+ default: { size: "default", vcpu: 4, memoryGb: 8, usdPerHour: 0.036 },
109
+ large: { size: "large", vcpu: 8, memoryGb: 16, usdPerHour: 0.072 }
110
+ }
111
+ };
112
+ var CLOUDFLARE_USD_PER_GIB_SECOND_MEMORY = 25e-7;
113
+ var CLOUDFLARE_USD_PER_GB_SECOND_DISK = 7e-8;
114
+ var CLOUDFLARE_USD_PER_VCPU_SECOND_ACTIVE = 2e-5;
115
+ var CLOUDFLARE_STANDARD_1 = { size: "standard-1", vcpu: 0.5, memoryGb: 4, diskGb: 8 };
116
+ var cloudflareBilling = {
117
+ source: { url: "https://developers.cloudflare.com/containers/pricing/", readOn: "2026-10-12" },
118
+ meter: "Cloudflare bills a container for every 10 ms it runs: memory at $0.0000025 per GiB-second and disk at $0.00000007 per GB-second for the whole run, plus CPU at $0.00002 per vCPU-second of active use.",
119
+ rates: {
120
+ default: {
121
+ size: CLOUDFLARE_STANDARD_1.size,
122
+ vcpu: CLOUDFLARE_STANDARD_1.vcpu,
123
+ memoryGb: CLOUDFLARE_STANDARD_1.memoryGb,
124
+ usdPerHour: (CLOUDFLARE_STANDARD_1.memoryGb * CLOUDFLARE_USD_PER_GIB_SECOND_MEMORY + CLOUDFLARE_STANDARD_1.diskGb * CLOUDFLARE_USD_PER_GB_SECOND_DISK) * HOUR_SECONDS
125
+ }
126
+ },
127
+ excludes: `active CPU, billed at $${CLOUDFLARE_USD_PER_VCPU_SECOND_ACTIVE} per vCPU-second and not measured by Claxedo, and the monthly allowance a Workers Paid plan includes`
128
+ };
129
+
130
+ // ../sandbox-contract/dist/index.mjs
131
+ var sandboxDriverCredentialFields = {
132
+ modal: [
133
+ { key: "token_id", label: "Token ID", secret: true },
134
+ { key: "token_secret", label: "Token Secret", secret: true }
135
+ ],
136
+ vercel: [
137
+ { key: "access_token", label: "Access Token", secret: true },
138
+ { key: "team_id", label: "Team ID" },
139
+ { key: "project_id", label: "Project ID" }
140
+ ],
141
+ cloudflare: [
142
+ { key: "api_token", label: "API Token", secret: true },
143
+ { key: "worker_url", label: "Worker URL" }
144
+ ],
145
+ boat: [{ key: "api_key", label: "API Key", secret: true }],
146
+ docker: [{ key: "image", label: "Image" }]
147
+ };
148
+ var sandboxDriverLabels = {
149
+ modal: "Modal",
150
+ vercel: "Vercel",
151
+ cloudflare: "Cloudflare",
152
+ boat: "Boat",
153
+ docker: "Docker"
154
+ };
91
155
 
92
156
  // src/driver-catalog.ts
93
157
  var sandboxDriverCatalog = {
94
- exe: {
95
- id: "exe",
96
- label: "exe.dev",
97
- credentialFields: [{ key: "api_token", label: "API Token", secret: true }],
98
- metadata: {
99
- driverRunsIn: ["worker", "node"],
100
- hostStopBehavior: "not-supported",
101
- hostResumeBehavior: "same-host",
102
- targetAccess: "relay",
103
- secretBrokering: "none",
104
- egressControl: "none",
105
- persistence: {
106
- resume: "same-sandbox",
107
- capture: "none",
108
- clone: true,
109
- captureSource: "not-applicable",
110
- retention: "provider-managed",
111
- restoreMount: "same-resource"
112
- }
113
- }
114
- },
115
- daytona: {
116
- id: "daytona",
117
- label: "Daytona",
118
- credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
119
- metadata: {
120
- driverRunsIn: ["worker", "node"],
121
- hostStopBehavior: "suspends-host",
122
- hostResumeBehavior: "same-host",
123
- targetAccess: "relay",
124
- secretBrokering: "native",
125
- // Daytona is the only driver that can filter egress by NAME as well as
126
- // by address: `domainAllowList` (names) alongside `networkAllowList`
127
- // (CIDRs), with `networkBlockAll` as the deny-all floor.
128
- egressControl: "hosts-and-cidrs",
129
- persistence: {
130
- resume: "same-sandbox",
131
- capture: "filesystem",
132
- clone: false,
133
- captureSource: "preserved",
134
- retention: "provider-managed",
135
- restoreMount: "new-resource"
136
- }
137
- }
138
- },
139
158
  modal: {
140
159
  id: "modal",
141
- label: "Modal",
142
- credentialFields: [
143
- { key: "token_id", label: "Token ID", secret: true },
144
- { key: "token_secret", label: "Token Secret", secret: true }
145
- ],
160
+ label: sandboxDriverLabels.modal,
161
+ credentialFields: sandboxDriverCredentialFields.modal,
162
+ runtimeNetwork: { controlPlane: "direct", relay: "configured" },
163
+ runtimeEnv: "callback",
164
+ preparedImage: true,
146
165
  metadata: {
147
166
  driverRunsIn: ["node"],
148
167
  hostStopBehavior: "terminates-host",
149
168
  hostResumeBehavior: "replacement-host",
150
169
  targetAccess: "relay",
151
170
  secretBrokering: "none",
152
- // Modal can cut the network entirely (`blockNetwork`) but cannot express
153
- // an allowlist, and its driver throws on a host policy. A blackout is
154
- // not containment for a workspace that has to clone and reach a model.
171
+ // The driver does not install Modal's domain allowlist or alpha sidecar.
155
172
  egressControl: "none",
156
173
  persistence: {
157
174
  resume: "replacement-restore",
@@ -165,20 +182,18 @@ var sandboxDriverCatalog = {
165
182
  },
166
183
  vercel: {
167
184
  id: "vercel",
168
- label: "Vercel",
169
- credentialFields: [
170
- { key: "access_token", label: "Access Token", secret: true },
171
- { key: "team_id", label: "Team ID" },
172
- { key: "project_id", label: "Project ID" }
173
- ],
185
+ label: sandboxDriverLabels.vercel,
186
+ credentialFields: sandboxDriverCredentialFields.vercel,
187
+ runtimeNetwork: { controlPlane: "direct", relay: "configured" },
188
+ runtimeEnv: "callback",
189
+ preparedImage: false,
174
190
  metadata: {
175
191
  driverRunsIn: ["node"],
176
192
  hostStopBehavior: "terminates-host",
177
193
  hostResumeBehavior: "replacement-host",
178
194
  targetAccess: "relay",
179
195
  secretBrokering: "native",
180
- // Vercel's sandbox firewall takes a hostname allow list (or "deny-all"),
181
- // never CIDRs.
196
+ // The driver translates hosts only; the SDK's subnet rules are not wired to net.cidrs.
182
197
  egressControl: "hosts",
183
198
  persistence: {
184
199
  resume: "replacement-restore",
@@ -192,40 +207,42 @@ var sandboxDriverCatalog = {
192
207
  },
193
208
  cloudflare: {
194
209
  id: "cloudflare",
195
- label: "Cloudflare",
196
- credentialFields: [
197
- { key: "api_token", label: "API Token", secret: true },
198
- { key: "worker_url", label: "Worker URL" }
199
- ],
210
+ label: sandboxDriverLabels.cloudflare,
211
+ credentialFields: sandboxDriverCredentialFields.cloudflare,
212
+ runtimeNetwork: { controlPlane: "direct", relay: "configured" },
213
+ runtimeEnv: "callback",
214
+ preparedImage: false,
215
+ billing: cloudflareBilling,
200
216
  metadata: {
201
217
  driverRunsIn: ["worker"],
202
- hostStopBehavior: "not-supported",
218
+ hostStopBehavior: "terminates-host",
203
219
  hostResumeBehavior: "same-host",
220
+ hostWatchesIdle: true,
204
221
  targetAccess: "relay",
205
- secretBrokering: "proxy",
206
- // The Cloudflare Sandbox Worker exposes no egress filter: its broker
207
- // (drivers/cloudflare-egress.ts) is an OPT-IN proxy the sandbox chooses
208
- // to route brokered-credential requests through, not a boundary. The
209
- // driver therefore drops `net` entirely — which is exactly why this
210
- // must be declared, so the manager refuses instead of provisioning a
211
- // sandbox the caller believes is contained.
222
+ secretBrokering: "native",
223
+ // Credential handlers do not restrict unrelated destinations.
212
224
  egressControl: "none",
225
+ // A container snapshot holds the whole root filesystem; the platform keeps it 30 days past its last restore and offers no deletion.
213
226
  persistence: {
214
227
  resume: "replacement-restore",
215
- capture: "directories",
228
+ capture: "filesystem",
216
229
  clone: false,
217
230
  captureSource: "preserved",
218
231
  retention: "provider-managed",
219
- restoreMount: "copy-on-write"
232
+ restoreMount: "new-resource"
220
233
  }
221
234
  }
222
235
  },
223
- box: {
224
- id: "box",
225
- label: "Box",
226
- credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
236
+ boat: {
237
+ id: "boat",
238
+ label: sandboxDriverLabels.boat,
239
+ credentialFields: sandboxDriverCredentialFields.boat,
240
+ runtimeNetwork: { controlPlane: "direct", relay: "configured" },
241
+ runtimeEnv: "callback",
242
+ preparedImage: true,
243
+ billing: boatBilling,
227
244
  metadata: {
228
- driverRunsIn: ["node"],
245
+ driverRunsIn: ["worker", "node"],
229
246
  hostStopBehavior: "suspends-host",
230
247
  hostResumeBehavior: "same-host",
231
248
  targetAccess: "relay",
@@ -238,13 +255,17 @@ var sandboxDriverCatalog = {
238
255
  captureSource: "not-applicable",
239
256
  retention: "provider-managed",
240
257
  restoreMount: "same-resource"
241
- }
258
+ },
259
+ machineClasses: ["small", "default", "large"]
242
260
  }
243
261
  },
244
262
  docker: {
245
263
  id: "docker",
246
- label: "Docker",
247
- credentialFields: [{ key: "image", label: "Image" }],
264
+ label: sandboxDriverLabels.docker,
265
+ credentialFields: sandboxDriverCredentialFields.docker,
266
+ runtimeNetwork: { controlPlane: "docker-host", relay: "configured" },
267
+ runtimeEnv: "callback",
268
+ preparedImage: true,
248
269
  metadata: {
249
270
  driverRunsIn: ["local"],
250
271
  hostStopBehavior: "terminates-host",
@@ -264,6 +285,40 @@ var sandboxDriverCatalog = {
264
285
  }
265
286
  };
266
287
 
288
+ // ../claxedo-helpers/dist/guards.mjs
289
+ function isRecord(value) {
290
+ return value !== null && typeof value === "object" && !Array.isArray(value);
291
+ }
292
+ function asRecord(value) {
293
+ return isRecord(value) ? value : void 0;
294
+ }
295
+
296
+ // src/json.ts
297
+ function num(input) {
298
+ return typeof input === "number" ? input : void 0;
299
+ }
300
+
301
+ // src/drivers/transient-error.ts
302
+ function textOf(input) {
303
+ return typeof input === "string" ? input : "";
304
+ }
305
+ function driverErrorSignals(err) {
306
+ const shaped = isRecord(err) ? err : {};
307
+ const nested = isRecord(shaped.response) ? shaped.response : {};
308
+ return {
309
+ // `response.status` is the axios shape; `statusCode` is what SDK-native
310
+ // error classes expose instead.
311
+ status: num(nested.status) ?? num(shaped.status) ?? num(shaped.statusCode),
312
+ text: `${textOf(shaped.code)} ${textOf(shaped.name)} ${textOf(shaped.message)}`.toLowerCase()
313
+ };
314
+ }
315
+ function isTransientDriverError(err, markers) {
316
+ const { status, text } = driverErrorSignals(err);
317
+ if (status !== void 0 && status >= 500) return true;
318
+ return markers.some((marker) => text.includes(marker));
319
+ }
320
+ var TRANSIENT_DRIVER_RETRY_MS = 2e3;
321
+
267
322
  // src/drivers/modal.ts
268
323
  var DEFAULT_APP_NAME = "claxedo";
269
324
  var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
@@ -271,21 +326,28 @@ var DEFAULT_WORKSPACE_DIR = "/workspace";
271
326
  var DEFAULT_TIMEOUT_MS = 30 * 60 * 1e3;
272
327
  var DEFAULT_TUNNEL_TIMEOUT_MS = 3e4;
273
328
  var DEFAULT_OPERATION_TIMEOUT_MS = 6e4;
329
+ function isModalClient(value) {
330
+ const row = asRecord(value);
331
+ return typeof asRecord(row?.apps)?.fromName === "function" && typeof asRecord(row?.images)?.fromRegistry === "function" && typeof asRecord(row?.sandboxes)?.create === "function";
332
+ }
274
333
  function nameFor(workspaceId) {
275
334
  return `claxedo-${workspaceId}`;
276
335
  }
336
+ var TRANSIENT_MARKERS = ["timeout", "unavailable", "deadline"];
277
337
  function transientDriverError(err) {
278
- const shaped = err;
279
- const status = shaped.response?.status ?? shaped.status;
280
- if (typeof status === "number" && status >= 500) return true;
281
- const text = `${shaped.code ?? ""} ${shaped.name ?? ""} ${shaped.message ?? ""}`.toLowerCase();
282
- return text.includes("timeout") || text.includes("unavailable") || text.includes("deadline");
338
+ return isTransientDriverError(err, TRANSIENT_MARKERS);
283
339
  }
284
340
  function createModalSandboxDriver(options) {
285
- const client = options.client ?? new ModalClient({
286
- tokenId: options.tokenId,
287
- tokenSecret: options.tokenSecret
288
- });
341
+ let defaultClient;
342
+ function resolveClient() {
343
+ if (options.client) return options.client;
344
+ defaultClient ??= import("modal").then((sdk) => {
345
+ const client = new sdk.ModalClient({ tokenId: options.tokenId, tokenSecret: options.tokenSecret });
346
+ if (!isModalClient(client)) throw new Error("modal SDK does not expose the client surface this driver uses");
347
+ return client;
348
+ });
349
+ return defaultClient;
350
+ }
289
351
  const appName = options.appName ?? DEFAULT_APP_NAME;
290
352
  const runtimePort = options.runtimePort ?? DEFAULT_WORKSPACE_RUNTIME_PORT;
291
353
  const runtimeCommand = options.runtimeCommand ?? DEFAULT_RUNTIME_COMMAND;
@@ -293,29 +355,28 @@ function createModalSandboxDriver(options) {
293
355
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
294
356
  const tunnelTimeoutMs = options.tunnelTimeoutMs ?? DEFAULT_TUNNEL_TIMEOUT_MS;
295
357
  const operationTimeoutMs = options.operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS;
296
- const readinessProbe = options.readinessProbe ?? ((port) => Probe.withTcp(port, { intervalMs: 1e3 }));
358
+ async function readinessProbeFor(port) {
359
+ if (options.readinessProbe) return options.readinessProbe(port);
360
+ const { Probe } = await import("modal");
361
+ return Probe.withTcp(port, { intervalMs: 1e3 });
362
+ }
297
363
  function workspaceDirectory(input) {
298
364
  return input.workspaceRoot ?? workspaceDir;
299
365
  }
300
366
  function bootEnv(input, hostId) {
301
- const env = {
302
- ...workspaceRuntimeTargetEnv({
303
- workspaceId: input.workspaceId,
304
- hostId,
305
- directory: workspaceDirectory(input),
306
- port: runtimePort,
307
- host: "0.0.0.0"
308
- }),
309
- ...workspaceRuntimeSourceEnv({ source: input.source }),
310
- ...input.env
311
- };
312
- if (options.runner) env.WORKSPACE_RUNTIME_RUNNER = options.runner;
313
- if (options.controlEnv?.relayJwksUrl) env.WORKSPACE_RUNTIME_RELAY_JWKS_URL = options.controlEnv.relayJwksUrl;
314
- if (options.controlEnv?.relayVerifyPem) env.WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM = options.controlEnv.relayVerifyPem;
315
- if (options.controlEnv?.managementJwksUrl) env.WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL = options.controlEnv.managementJwksUrl;
316
- return env;
367
+ return workspaceRuntimeBootEnv({
368
+ workspaceId: input.workspaceId,
369
+ hostId,
370
+ directory: workspaceDirectory(input),
371
+ port: runtimePort,
372
+ host: "0.0.0.0",
373
+ source: input.source,
374
+ env: input.env,
375
+ nativeHarness: options.nativeHarness,
376
+ controlEnv: options.controlEnv
377
+ });
317
378
  }
318
- async function image(input) {
379
+ async function image(client, input) {
319
380
  if (input.bootSource?.kind === "driver-snapshot") return client.images.fromId(input.bootSource.snapshotId);
320
381
  if (input.bootSource?.kind === "image") return client.images.fromId(input.bootSource.image);
321
382
  if (input.snapshot) return client.images.fromId(input.snapshot);
@@ -329,10 +390,13 @@ function createModalSandboxDriver(options) {
329
390
  return { blockNetwork: true };
330
391
  }
331
392
  async function readyTarget(input, sandbox, hostId) {
332
- await sandbox.setTags(input.labels).catch(() => void 0);
333
- const tunnels = await sandbox.tunnels(tunnelTimeoutMs).catch(() => void 0);
393
+ await sandbox.setTags(input.labels);
394
+ const tunnels = await sandbox.tunnels(tunnelTimeoutMs).catch((error) => {
395
+ if (error instanceof Error && error.name === "SandboxTimeoutError") return void 0;
396
+ throw error;
397
+ });
334
398
  const tunnel = tunnels?.[runtimePort];
335
- if (!tunnel?.url) return { provisioning: true, retryAfterMs: 2e3 };
399
+ if (!tunnel?.url) return { provisioning: true, retryAfterMs: 0 };
336
400
  return {
337
401
  workspaceId: input.workspaceId,
338
402
  sandboxId: sandbox.sandboxId,
@@ -353,9 +417,10 @@ function createModalSandboxDriver(options) {
353
417
  ...bootEnv(input, hostId),
354
418
  ...await options.env?.(input, { id: hostId })
355
419
  };
420
+ const client = await resolveClient();
356
421
  const sandbox = await client.sandboxes.create(
357
422
  await client.apps.fromName(appName, { createIfMissing: true }),
358
- await image(input),
423
+ await image(client, input),
359
424
  {
360
425
  name: nameFor(input.workspaceId),
361
426
  command: ["bash", "-lc", `mkdir -p ${shell(directory)}; cd ${shell(directory)}; exec ${runtimeCommand}`],
@@ -365,18 +430,18 @@ function createModalSandboxDriver(options) {
365
430
  timeoutMs,
366
431
  ...options.idleTimeoutMs !== void 0 ? { idleTimeoutMs: options.idleTimeoutMs } : {},
367
432
  tags: { ...input.labels, "claxedo.workspaceId": input.workspaceId },
368
- readinessProbe: readinessProbe(runtimePort),
433
+ readinessProbe: await readinessProbeFor(runtimePort),
369
434
  ...network(input)
370
435
  }
371
436
  ).catch((err) => {
372
437
  if (transientDriverError(err)) return void 0;
373
438
  throw err;
374
439
  });
375
- if (!sandbox) return { provisioning: true, retryAfterMs: 2e3 };
440
+ if (!sandbox) return { provisioning: true, retryAfterMs: TRANSIENT_DRIVER_RETRY_MS };
376
441
  return readyTarget(input, sandbox, hostId);
377
442
  }
378
443
  async function sandboxById(target) {
379
- return client.sandboxes.fromId(target.sandboxId);
444
+ return (await resolveClient()).sandboxes.fromId(target.sandboxId);
380
445
  }
381
446
  return {
382
447
  id: "modal",
@@ -0,0 +1,13 @@
1
+ /** The status and lower-cased text a driver error carries, if it carries any. */
2
+ export declare function driverErrorSignals(err: unknown): {
3
+ status: number | undefined;
4
+ text: string;
5
+ };
6
+ /**
7
+ * A 5xx is transient for every driver; anything else is a per-driver judgment
8
+ * made from the error text, so each driver passes the markers it has actually
9
+ * seen its own SDK produce.
10
+ */
11
+ export declare function isTransientDriverError(err: unknown, markers: readonly string[]): boolean;
12
+ /** How long a driver waits before asking a provider again after a failure `isTransientDriverError` admits, so a provider in trouble is not asked at once. */
13
+ export declare const TRANSIENT_DRIVER_RETRY_MS = 2000;
@@ -1,5 +1,7 @@
1
- import { type NetworkPolicy } from "@vercel/sandbox";
2
- import type { SandboxBrokeredSecret, SandboxDriver, SandboxDriverEnsureInput } from "..";
1
+ import type { NetworkPolicy } from "@vercel/sandbox";
2
+ import { type SandboxDriver, type SandboxDriverEnsureInput } from "../contract";
3
+ import { type SandboxBrokeredSecret } from "../egress-policy";
4
+ import { type WorkspaceRuntimeControlEnv } from "../runtime-env";
3
5
  type VercelSnapshotLike = {
4
6
  snapshotId: string;
5
7
  };
@@ -79,12 +81,8 @@ export type VercelSandboxDriverOptions = {
79
81
  runtimePort?: number;
80
82
  runtimeCommand?: string;
81
83
  workspaceDir?: string;
82
- runner?: string;
83
- controlEnv?: {
84
- relayJwksUrl?: string;
85
- relayVerifyPem?: string;
86
- managementJwksUrl?: string;
87
- };
84
+ nativeHarness?: string;
85
+ controlEnv?: WorkspaceRuntimeControlEnv;
88
86
  env?: (input: SandboxDriverEnsureInput, host: {
89
87
  id: string;
90
88
  }) => Record<string, string> | Promise<Record<string, string>>;
@@ -103,26 +101,26 @@ export type VercelSandboxDriverOptions = {
103
101
  * and https://vercel.com/docs/sandbox/concepts/firewall — credentials
104
102
  * brokering).
105
103
  *
106
- * The result is the UNION of `base` — the create-time allow-list — and the
104
+ * The result is the union of `base` — the create-time allow-list — and the
107
105
  * brokered hosts, never a replacement. Replacing it is wrong in both
108
- * directions. It BREAKS egress the caller was granted: `updateNetworkPolicy`
109
- * overwrites the whole policy, so a sandbox created with git and the npm
110
- * registry allowed loses both the instant a brokered secret is attached, and
111
- * the agent's next `npm install` fails mid-run. And it can silently WIDEN
112
- * egress: a brokered host that was not in the create-time list becomes
113
- * reachable, so a policy nobody approved is installed as a side effect of
114
- * attaching a credential. A union changes exactly one thing — it adds the
115
- * brokered hosts, which is the smallest edit that makes the credential usable.
106
+ * directions: it breaks egress the caller was granted, since
107
+ * `updateNetworkPolicy` overwrites the whole policy, so a sandbox created with
108
+ * git and the npm registry allowed loses both the instant a brokered secret is
109
+ * attached, and the agent's next `npm install` fails mid-run. It can also
110
+ * silently widen egress, since a brokered host that was not in the create-time
111
+ * list becomes reachable, installing a policy nobody approved as a side effect
112
+ * of attaching a credential. A union changes exactly one thing — it adds the
113
+ * brokered hosts, the smallest edit that makes the credential usable.
116
114
  *
117
115
  * `base` is the create-time `NetworkPolicy` exactly as passed to `create`, and
118
- * it is REQUIRED even though `undefined` is a legal value: `undefined` means
116
+ * it is required even though `undefined` is a legal value: `undefined` means
119
117
  * "no policy was requested", so the merge keeps egress unrestricted, expressed
120
118
  * as the `"*"` wildcard the record form needs for the header transforms to ride
121
- * alongside it. That is the opposite of what this function used to return with
122
- * no base at all (deny-all-except-brokered-hosts), so an optional parameter
123
- * would let an existing caller keep compiling while its egress silently WIDENS.
124
- * Spelling the base out forces that decision to be made once, visibly. A
125
- * `"deny-all"` base contributes nothing, leaving deny-all-except-brokered-hosts.
119
+ * alongside it. An optional parameter defaulting to
120
+ * deny-all-except-brokered-hosts would let an existing caller keep compiling
121
+ * while its egress silently widens. Spelling the base out forces that decision
122
+ * to be made once, visibly. A `"deny-all"` base contributes nothing, leaving
123
+ * deny-all-except-brokered-hosts.
126
124
  */
127
125
  export declare function vercelBrokeredNetworkPolicy(secrets: SandboxBrokeredSecret[], base: NetworkPolicy | undefined): NetworkPolicy;
128
126
  export declare function createVercelSandboxDriver(options: VercelSandboxDriverOptions): SandboxDriver;