@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,7 +1,55 @@
1
- // src/drivers/vercel.ts
2
- import { Sandbox } from "@vercel/sandbox";
1
+ // src/egress-policy.ts
2
+ function brokeredSecretPlaceholder(name) {
3
+ return `claxedo-broker:${name}`;
4
+ }
5
+ function brokeredPlaceholderEnv(secrets) {
6
+ return Object.fromEntries((secrets ?? []).map((secret) => [secret.name, brokeredSecretPlaceholder(secret.name)]));
7
+ }
3
8
 
4
9
  // src/runtime-env.ts
10
+ var WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS = [
11
+ "WORKSPACE_RUNTIME_WORKSPACE_ID",
12
+ "WORKSPACE_RUNTIME_HOST_ID",
13
+ "WORKSPACE_RUNTIME_RELAY_WORKSPACE_IDS"
14
+ ];
15
+ function workspaceRuntimeIdentityEnvConflicts(env) {
16
+ if (!env) return [];
17
+ return WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS.filter((key) => env[key] !== void 0);
18
+ }
19
+ function assertWorkspaceRuntimeIdentityEnv(env) {
20
+ const conflicts = workspaceRuntimeIdentityEnvConflicts(env);
21
+ if (conflicts.length === 0) return;
22
+ throw new Error(
23
+ `sandbox env cannot set runtime identity ${conflicts.join(", ")}: the driver's placement owns the identity its target reports and the lease records`
24
+ );
25
+ }
26
+ var WORKSPACE_RUNTIME_PUBLIC_ENV_KEYS = /* @__PURE__ */ new Set([
27
+ ...WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS,
28
+ "WORKSPACE_RUNTIME_DIRECTORY",
29
+ "WORKSPACE_RUNTIME_PORT",
30
+ "WORKSPACE_RUNTIME_HOST",
31
+ "WORKSPACE_RUNTIME_SOURCE_KIND",
32
+ "WORKSPACE_RUNTIME_GIT_REPO_URL",
33
+ "WORKSPACE_RUNTIME_GIT_BRANCH",
34
+ "WORKSPACE_RUNTIME_NATIVE_HARNESS",
35
+ "WORKSPACE_RUNTIME_RELAY_JWKS_URL",
36
+ "WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL",
37
+ "WORKSPACE_RUNTIME_SESSION_AUTHORITY_URL"
38
+ ]);
39
+ function workspaceRuntimeBootEnv(input) {
40
+ assertWorkspaceRuntimeIdentityEnv(input.env);
41
+ const env = {
42
+ ...workspaceRuntimeTargetEnv(input),
43
+ ...workspaceRuntimeSourceEnv(input),
44
+ ...input.env
45
+ };
46
+ if (input.nativeHarness) env.WORKSPACE_RUNTIME_NATIVE_HARNESS = input.nativeHarness;
47
+ if (input.controlEnv?.relayJwksUrl) env.WORKSPACE_RUNTIME_RELAY_JWKS_URL = input.controlEnv.relayJwksUrl;
48
+ if (input.controlEnv?.relayVerifyPem) env.WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM = input.controlEnv.relayVerifyPem;
49
+ if (input.controlEnv?.managementJwksUrl) env.WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL = input.controlEnv.managementJwksUrl;
50
+ if (input.controlEnv?.sessionAuthorityUrl) env.WORKSPACE_RUNTIME_SESSION_AUTHORITY_URL = input.controlEnv.sessionAuthorityUrl;
51
+ return env;
52
+ }
5
53
  function workspaceRuntimeTargetEnv(input) {
6
54
  return {
7
55
  WORKSPACE_RUNTIME_WORKSPACE_ID: input.workspaceId,
@@ -37,95 +85,77 @@ var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
37
85
  // src/defaults.ts
38
86
  var RUNTIME_DIR = "/opt/workspace-runtime";
39
87
 
40
- // src/hosted-network-policy.ts
41
- var SANDBOX_MODEL_PROVIDER_HOSTS = [
42
- "api.anthropic.com",
43
- "api.openai.com",
44
- "generativelanguage.googleapis.com",
45
- "openrouter.ai",
46
- // Model catalog + engine config, fetched on runner startup.
47
- "models.dev",
48
- "opencode.ai"
49
- ];
50
- var SANDBOX_PACKAGE_REGISTRY_HOSTS = [
51
- "registry.npmjs.org",
52
- "nodejs.org",
53
- "pypi.org",
54
- "files.pythonhosted.org",
55
- "crates.io",
56
- "static.crates.io",
57
- "index.crates.io",
58
- "proxy.golang.org",
59
- "sum.golang.org"
60
- ];
61
- var SANDBOX_BASELINE_EGRESS_HOSTS = [
62
- ...SANDBOX_MODEL_PROVIDER_HOSTS,
63
- ...SANDBOX_PACKAGE_REGISTRY_HOSTS
64
- ];
88
+ // src/driver-billing.ts
89
+ var HOUR_SECONDS = 3600;
90
+ var boatBilling = {
91
+ source: { url: "https://docs.boat.dev/pricing", readOn: "2026-10-12" },
92
+ meter: "Boat bills machine time per second while a sandbox runs, at an hourly rate per size; a stopped sandbox costs nothing.",
93
+ rates: {
94
+ small: { size: "small", vcpu: 2, memoryGb: 4, usdPerHour: 0.018 },
95
+ default: { size: "default", vcpu: 4, memoryGb: 8, usdPerHour: 0.036 },
96
+ large: { size: "large", vcpu: 8, memoryGb: 16, usdPerHour: 0.072 }
97
+ }
98
+ };
99
+ var CLOUDFLARE_USD_PER_GIB_SECOND_MEMORY = 25e-7;
100
+ var CLOUDFLARE_USD_PER_GB_SECOND_DISK = 7e-8;
101
+ var CLOUDFLARE_USD_PER_VCPU_SECOND_ACTIVE = 2e-5;
102
+ var CLOUDFLARE_STANDARD_1 = { size: "standard-1", vcpu: 0.5, memoryGb: 4, diskGb: 8 };
103
+ var cloudflareBilling = {
104
+ source: { url: "https://developers.cloudflare.com/containers/pricing/", readOn: "2026-10-12" },
105
+ 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.",
106
+ rates: {
107
+ default: {
108
+ size: CLOUDFLARE_STANDARD_1.size,
109
+ vcpu: CLOUDFLARE_STANDARD_1.vcpu,
110
+ memoryGb: CLOUDFLARE_STANDARD_1.memoryGb,
111
+ usdPerHour: (CLOUDFLARE_STANDARD_1.memoryGb * CLOUDFLARE_USD_PER_GIB_SECOND_MEMORY + CLOUDFLARE_STANDARD_1.diskGb * CLOUDFLARE_USD_PER_GB_SECOND_DISK) * HOUR_SECONDS
112
+ }
113
+ },
114
+ 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`
115
+ };
116
+
117
+ // ../sandbox-contract/dist/index.mjs
118
+ var sandboxDriverCredentialFields = {
119
+ modal: [
120
+ { key: "token_id", label: "Token ID", secret: true },
121
+ { key: "token_secret", label: "Token Secret", secret: true }
122
+ ],
123
+ vercel: [
124
+ { key: "access_token", label: "Access Token", secret: true },
125
+ { key: "team_id", label: "Team ID" },
126
+ { key: "project_id", label: "Project ID" }
127
+ ],
128
+ cloudflare: [
129
+ { key: "api_token", label: "API Token", secret: true },
130
+ { key: "worker_url", label: "Worker URL" }
131
+ ],
132
+ boat: [{ key: "api_key", label: "API Key", secret: true }],
133
+ docker: [{ key: "image", label: "Image" }]
134
+ };
135
+ var sandboxDriverLabels = {
136
+ modal: "Modal",
137
+ vercel: "Vercel",
138
+ cloudflare: "Cloudflare",
139
+ boat: "Boat",
140
+ docker: "Docker"
141
+ };
65
142
 
66
143
  // src/driver-catalog.ts
67
144
  var sandboxDriverCatalog = {
68
- exe: {
69
- id: "exe",
70
- label: "exe.dev",
71
- credentialFields: [{ key: "api_token", label: "API Token", secret: true }],
72
- metadata: {
73
- driverRunsIn: ["worker", "node"],
74
- hostStopBehavior: "not-supported",
75
- hostResumeBehavior: "same-host",
76
- targetAccess: "relay",
77
- secretBrokering: "none",
78
- egressControl: "none",
79
- persistence: {
80
- resume: "same-sandbox",
81
- capture: "none",
82
- clone: true,
83
- captureSource: "not-applicable",
84
- retention: "provider-managed",
85
- restoreMount: "same-resource"
86
- }
87
- }
88
- },
89
- daytona: {
90
- id: "daytona",
91
- label: "Daytona",
92
- credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
93
- metadata: {
94
- driverRunsIn: ["worker", "node"],
95
- hostStopBehavior: "suspends-host",
96
- hostResumeBehavior: "same-host",
97
- targetAccess: "relay",
98
- secretBrokering: "native",
99
- // Daytona is the only driver that can filter egress by NAME as well as
100
- // by address: `domainAllowList` (names) alongside `networkAllowList`
101
- // (CIDRs), with `networkBlockAll` as the deny-all floor.
102
- egressControl: "hosts-and-cidrs",
103
- persistence: {
104
- resume: "same-sandbox",
105
- capture: "filesystem",
106
- clone: false,
107
- captureSource: "preserved",
108
- retention: "provider-managed",
109
- restoreMount: "new-resource"
110
- }
111
- }
112
- },
113
145
  modal: {
114
146
  id: "modal",
115
- label: "Modal",
116
- credentialFields: [
117
- { key: "token_id", label: "Token ID", secret: true },
118
- { key: "token_secret", label: "Token Secret", secret: true }
119
- ],
147
+ label: sandboxDriverLabels.modal,
148
+ credentialFields: sandboxDriverCredentialFields.modal,
149
+ runtimeNetwork: { controlPlane: "direct", relay: "configured" },
150
+ runtimeEnv: "callback",
151
+ preparedImage: true,
120
152
  metadata: {
121
153
  driverRunsIn: ["node"],
122
154
  hostStopBehavior: "terminates-host",
123
155
  hostResumeBehavior: "replacement-host",
124
156
  targetAccess: "relay",
125
157
  secretBrokering: "none",
126
- // Modal can cut the network entirely (`blockNetwork`) but cannot express
127
- // an allowlist, and its driver throws on a host policy. A blackout is
128
- // not containment for a workspace that has to clone and reach a model.
158
+ // The driver does not install Modal's domain allowlist or alpha sidecar.
129
159
  egressControl: "none",
130
160
  persistence: {
131
161
  resume: "replacement-restore",
@@ -139,20 +169,18 @@ var sandboxDriverCatalog = {
139
169
  },
140
170
  vercel: {
141
171
  id: "vercel",
142
- label: "Vercel",
143
- credentialFields: [
144
- { key: "access_token", label: "Access Token", secret: true },
145
- { key: "team_id", label: "Team ID" },
146
- { key: "project_id", label: "Project ID" }
147
- ],
172
+ label: sandboxDriverLabels.vercel,
173
+ credentialFields: sandboxDriverCredentialFields.vercel,
174
+ runtimeNetwork: { controlPlane: "direct", relay: "configured" },
175
+ runtimeEnv: "callback",
176
+ preparedImage: false,
148
177
  metadata: {
149
178
  driverRunsIn: ["node"],
150
179
  hostStopBehavior: "terminates-host",
151
180
  hostResumeBehavior: "replacement-host",
152
181
  targetAccess: "relay",
153
182
  secretBrokering: "native",
154
- // Vercel's sandbox firewall takes a hostname allow list (or "deny-all"),
155
- // never CIDRs.
183
+ // The driver translates hosts only; the SDK's subnet rules are not wired to net.cidrs.
156
184
  egressControl: "hosts",
157
185
  persistence: {
158
186
  resume: "replacement-restore",
@@ -166,40 +194,42 @@ var sandboxDriverCatalog = {
166
194
  },
167
195
  cloudflare: {
168
196
  id: "cloudflare",
169
- label: "Cloudflare",
170
- credentialFields: [
171
- { key: "api_token", label: "API Token", secret: true },
172
- { key: "worker_url", label: "Worker URL" }
173
- ],
197
+ label: sandboxDriverLabels.cloudflare,
198
+ credentialFields: sandboxDriverCredentialFields.cloudflare,
199
+ runtimeNetwork: { controlPlane: "direct", relay: "configured" },
200
+ runtimeEnv: "callback",
201
+ preparedImage: false,
202
+ billing: cloudflareBilling,
174
203
  metadata: {
175
204
  driverRunsIn: ["worker"],
176
- hostStopBehavior: "not-supported",
205
+ hostStopBehavior: "terminates-host",
177
206
  hostResumeBehavior: "same-host",
207
+ hostWatchesIdle: true,
178
208
  targetAccess: "relay",
179
- secretBrokering: "proxy",
180
- // The Cloudflare Sandbox Worker exposes no egress filter: its broker
181
- // (drivers/cloudflare-egress.ts) is an OPT-IN proxy the sandbox chooses
182
- // to route brokered-credential requests through, not a boundary. The
183
- // driver therefore drops `net` entirely — which is exactly why this
184
- // must be declared, so the manager refuses instead of provisioning a
185
- // sandbox the caller believes is contained.
209
+ secretBrokering: "native",
210
+ // Credential handlers do not restrict unrelated destinations.
186
211
  egressControl: "none",
212
+ // A container snapshot holds the whole root filesystem; the platform keeps it 30 days past its last restore and offers no deletion.
187
213
  persistence: {
188
214
  resume: "replacement-restore",
189
- capture: "directories",
215
+ capture: "filesystem",
190
216
  clone: false,
191
217
  captureSource: "preserved",
192
218
  retention: "provider-managed",
193
- restoreMount: "copy-on-write"
219
+ restoreMount: "new-resource"
194
220
  }
195
221
  }
196
222
  },
197
- box: {
198
- id: "box",
199
- label: "Box",
200
- credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
223
+ boat: {
224
+ id: "boat",
225
+ label: sandboxDriverLabels.boat,
226
+ credentialFields: sandboxDriverCredentialFields.boat,
227
+ runtimeNetwork: { controlPlane: "direct", relay: "configured" },
228
+ runtimeEnv: "callback",
229
+ preparedImage: true,
230
+ billing: boatBilling,
201
231
  metadata: {
202
- driverRunsIn: ["node"],
232
+ driverRunsIn: ["worker", "node"],
203
233
  hostStopBehavior: "suspends-host",
204
234
  hostResumeBehavior: "same-host",
205
235
  targetAccess: "relay",
@@ -212,13 +242,17 @@ var sandboxDriverCatalog = {
212
242
  captureSource: "not-applicable",
213
243
  retention: "provider-managed",
214
244
  restoreMount: "same-resource"
215
- }
245
+ },
246
+ machineClasses: ["small", "default", "large"]
216
247
  }
217
248
  },
218
249
  docker: {
219
250
  id: "docker",
220
- label: "Docker",
221
- credentialFields: [{ key: "image", label: "Image" }],
251
+ label: sandboxDriverLabels.docker,
252
+ credentialFields: sandboxDriverCredentialFields.docker,
253
+ runtimeNetwork: { controlPlane: "docker-host", relay: "configured" },
254
+ runtimeEnv: "callback",
255
+ preparedImage: true,
222
256
  metadata: {
223
257
  driverRunsIn: ["local"],
224
258
  hostStopBehavior: "terminates-host",
@@ -238,8 +272,60 @@ var sandboxDriverCatalog = {
238
272
  }
239
273
  };
240
274
 
275
+ // ../claxedo-helpers/dist/guards.mjs
276
+ function isRecord(value) {
277
+ return value !== null && typeof value === "object" && !Array.isArray(value);
278
+ }
279
+ function asRecord(value) {
280
+ return isRecord(value) ? value : void 0;
281
+ }
282
+
283
+ // src/json.ts
284
+ function num(input) {
285
+ return typeof input === "number" ? input : void 0;
286
+ }
287
+
288
+ // src/drivers/transient-error.ts
289
+ function textOf(input) {
290
+ return typeof input === "string" ? input : "";
291
+ }
292
+ function driverErrorSignals(err) {
293
+ const shaped = isRecord(err) ? err : {};
294
+ const nested = isRecord(shaped.response) ? shaped.response : {};
295
+ return {
296
+ // `response.status` is the axios shape; `statusCode` is what SDK-native
297
+ // error classes expose instead.
298
+ status: num(nested.status) ?? num(shaped.status) ?? num(shaped.statusCode),
299
+ text: `${textOf(shaped.code)} ${textOf(shaped.name)} ${textOf(shaped.message)}`.toLowerCase()
300
+ };
301
+ }
302
+ function isTransientDriverError(err, markers) {
303
+ const { status, text } = driverErrorSignals(err);
304
+ if (status !== void 0 && status >= 500) return true;
305
+ return markers.some((marker) => text.includes(marker));
306
+ }
307
+ var TRANSIENT_DRIVER_RETRY_MS = 2e3;
308
+
309
+ // ../claxedo-helpers/dist/deadline.mjs
310
+ async function withDeadlineSignal(timeoutMs, work, parent) {
311
+ const deadline = new AbortController();
312
+ const timer = setTimeout(() => deadline.abort(new DOMException("The operation timed out.", "TimeoutError")), timeoutMs);
313
+ try {
314
+ return await work(parent ? AbortSignal.any([parent, deadline.signal]) : deadline.signal);
315
+ } finally {
316
+ clearTimeout(timer);
317
+ }
318
+ }
319
+
241
320
  // src/drivers/vercel.ts
242
321
  var DEFAULT_RUNTIME = "node22";
322
+ var HARNESS_RELEASE_AGE_EXCLUSIONS = `printf '%s\\n' ${[
323
+ "@agentclientprotocol/sdk",
324
+ "@anthropic-ai/claude-*",
325
+ "@cursor/sdk*",
326
+ "@earendil-works/*",
327
+ "@openai/codex"
328
+ ].map((name) => `'min-release-age-exclude[]=${name}'`).join(" ")} >> "$HOME/.npmrc"`;
243
329
  var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
244
330
  var DEFAULT_WORKSPACE_DIR = "/workspace";
245
331
  var DEFAULT_TIMEOUT_MS = 30 * 60 * 1e3;
@@ -252,15 +338,16 @@ function runtimePackage(version) {
252
338
  var SNAPSHOT_CACHE_TTL_MS = 60 * 60 * 1e3;
253
339
  var snapshotCache = /* @__PURE__ */ new Map();
254
340
  var snapshotCacheState = /* @__PURE__ */ new WeakMap();
341
+ function isVercelSandboxFactory(value) {
342
+ const row = asRecord(value);
343
+ return typeof row?.create === "function" && typeof row.get === "function";
344
+ }
255
345
  function nameFor(workspaceId) {
256
346
  return `claxedo-${workspaceId}`;
257
347
  }
348
+ var TRANSIENT_MARKERS = ["timeout", "unavailable", "deadline", "pending"];
258
349
  function transientDriverError(err) {
259
- const shaped = err;
260
- const status = shaped.response?.status ?? shaped.status;
261
- if (typeof status === "number" && status >= 500) return true;
262
- const text = `${shaped.code ?? ""} ${shaped.name ?? ""} ${shaped.message ?? ""}`.toLowerCase();
263
- return text.includes("timeout") || text.includes("unavailable") || text.includes("deadline") || text.includes("pending");
350
+ return isTransientDriverError(err, TRANSIENT_MARKERS);
264
351
  }
265
352
  function network(input) {
266
353
  if (!input.net || input.net.mode === "allow-all") return void 0;
@@ -277,14 +364,42 @@ function vercelBrokeredNetworkPolicy(secrets, base) {
277
364
  `vercel brokered secret "${secret.name}" requires a header \u2014 the firewall injects the value as an HTTP header on egress`
278
365
  );
279
366
  }
367
+ if (secret.hosts.length === 0) {
368
+ throw new Error(`vercel brokered secret "${secret.name}" requires at least one host in its egress allowlist`);
369
+ }
370
+ const value = secret.scheme ? `${secret.scheme} ${secret.value}` : secret.value;
371
+ const methods = [...secret.methods ?? []];
372
+ const pathPrefixes = [...secret.pathPrefixes ?? []];
280
373
  for (const host of secret.hosts) {
281
- ;
282
- (allow[host] ??= []).push({ transform: [{ headers: { [secret.header]: secret.value } }] });
374
+ const rules = allow[host] ??= [];
375
+ for (const prefix of methods.length ? pathPrefixes : []) {
376
+ const path = credentialPathPattern(prefix);
377
+ if (path === void 0) continue;
378
+ rules.push({
379
+ match: {
380
+ path: { regex: path },
381
+ method: methods,
382
+ headers: [{ key: { exact: secret.header.toLowerCase() }, value: { exact: `${secret.scheme ? `${secret.scheme} ` : ""}claxedo-broker:${secret.name}` } }]
383
+ },
384
+ transform: [{ headers: { [secret.header]: value } }]
385
+ });
386
+ }
283
387
  }
284
388
  }
285
389
  const subnets = typeof base === "object" && base?.subnets ? { subnets: base.subnets } : {};
286
390
  return { allow, ...subnets };
287
391
  }
392
+ var RE2_SPECIAL = /[\\.+*?()|[\]{}^$]/g;
393
+ var PATH_CHAR = "(?:[^/\\\\%?#]|%(?:2[0-46-9a-dA-D]|5[0-9abd-fABD-F]|[013-46-9a-fA-F][0-9a-fA-F]))";
394
+ var PATH_CHAR_NOT_DOT = "(?:[^./\\\\%?#]|%(?:2[0-46-9a-dA-D]|5[0-9abd-fABD-F]|[013-46-9a-fA-F][0-9a-fA-F]))";
395
+ var SEGMENT = `(?:(?:${PATH_CHAR_NOT_DOT}|\\.${PATH_CHAR_NOT_DOT}|\\.\\.${PATH_CHAR})${PATH_CHAR}*)?`;
396
+ function credentialPathPattern(prefix) {
397
+ if (!new RegExp(`^(?:/${SEGMENT})+$`).test(prefix)) return void 0;
398
+ const base = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
399
+ const literal = base.replace(RE2_SPECIAL, "\\$&");
400
+ const rest = prefix.endsWith("/") ? `/${SEGMENT}(?:/${SEGMENT})*` : `(?:/${SEGMENT})*`;
401
+ return `^${literal}${rest}(?:\\?.*)?$`;
402
+ }
288
403
  function baseAllowEntries(base) {
289
404
  if (base === void 0 || base === "allow-all") return [["*", []]];
290
405
  if (base === "deny-all") return [];
@@ -292,7 +407,18 @@ function baseAllowEntries(base) {
292
407
  return Object.entries(base.allow ?? {});
293
408
  }
294
409
  function createVercelSandboxDriver(options) {
295
- const sandboxFactory = options.sandbox ?? Sandbox;
410
+ let defaultFactory;
411
+ function resolveFactory() {
412
+ if (options.sandbox) return options.sandbox;
413
+ defaultFactory ??= import("@vercel/sandbox").then((sdk) => {
414
+ const factory = sdk.Sandbox;
415
+ if (!isVercelSandboxFactory(factory)) {
416
+ throw new Error("@vercel/sandbox does not expose the factory surface this driver uses");
417
+ }
418
+ return factory;
419
+ });
420
+ return defaultFactory;
421
+ }
296
422
  const runtime = options.runtime ?? DEFAULT_RUNTIME;
297
423
  const runtimePort = options.runtimePort ?? DEFAULT_WORKSPACE_RUNTIME_PORT;
298
424
  const runtimeCommand = options.runtimeCommand ?? DEFAULT_RUNTIME_COMMAND;
@@ -319,27 +445,20 @@ function createVercelSandboxDriver(options) {
319
445
  };
320
446
  }
321
447
  function bootEnv(input, hostId) {
322
- const env = {
323
- ...workspaceRuntimeTargetEnv({
324
- workspaceId: input.workspaceId,
325
- hostId,
326
- directory: workspaceDirectory(input),
327
- port: runtimePort,
328
- host: "0.0.0.0"
329
- }),
330
- ...workspaceRuntimeSourceEnv({ source: input.source }),
331
- ...input.env
332
- };
333
- if (options.runner) env.WORKSPACE_RUNTIME_RUNNER = options.runner;
334
- if (options.controlEnv?.relayJwksUrl) env.WORKSPACE_RUNTIME_RELAY_JWKS_URL = options.controlEnv.relayJwksUrl;
335
- if (options.controlEnv?.relayVerifyPem) env.WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM = options.controlEnv.relayVerifyPem;
336
- if (options.controlEnv?.managementJwksUrl) env.WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL = options.controlEnv.managementJwksUrl;
337
- return env;
448
+ return workspaceRuntimeBootEnv({
449
+ workspaceId: input.workspaceId,
450
+ hostId,
451
+ directory: workspaceDirectory(input),
452
+ port: runtimePort,
453
+ host: "0.0.0.0",
454
+ source: input.source,
455
+ env: input.env,
456
+ nativeHarness: options.nativeHarness,
457
+ controlEnv: options.controlEnv
458
+ });
338
459
  }
339
460
  async function runSetup(sandbox, command) {
340
- await sandbox.runCommand("bash", ["-c", command], {
341
- signal: AbortSignal.timeout(snapshotBuildTimeoutMs)
342
- });
461
+ await withDeadlineSignal(snapshotBuildTimeoutMs, (signal) => sandbox.runCommand("bash", ["-c", command], { signal }));
343
462
  }
344
463
  async function ensureSnapshot() {
345
464
  if (options.baseSnapshotId) return options.baseSnapshotId;
@@ -355,7 +474,7 @@ function createVercelSandboxDriver(options) {
355
474
  const running = cacheState.builds.get(cacheKey);
356
475
  if (running) return running;
357
476
  const build = (async () => {
358
- const builder = await sandboxFactory.create({
477
+ const builder = await (await resolveFactory()).create({
359
478
  ...credentials(),
360
479
  runtime,
361
480
  ports: [runtimePort],
@@ -363,7 +482,8 @@ function createVercelSandboxDriver(options) {
363
482
  });
364
483
  await runSetup(builder, "sudo dnf install -y git make gcc-c++ python3 > /dev/null 2>&1");
365
484
  await runSetup(builder, [
366
- "npm i -g --min-release-age=2 tsx@4.22.3 opencode-ai@1.15.10 @anthropic-ai/claude-code@2.1.150 @openai/codex@0.133.0 @google/gemini-cli@0.43.0 @earendil-works/pi-coding-agent@0.75.5",
485
+ HARNESS_RELEASE_AGE_EXCLUSIONS,
486
+ "npm i -g --min-release-age=2 tsx@4.22.3 opencode-ai@1.15.10 @anthropic-ai/claude-code@2.1.285 @openai/codex@0.159.2",
367
487
  "curl https://cursor.com/install -fsS | bash",
368
488
  "curl -fsSL https://ampcode.com/install.sh | bash",
369
489
  "curl -fsSL https://app.factory.ai/cli | sh",
@@ -374,12 +494,10 @@ function createVercelSandboxDriver(options) {
374
494
  "command -v opencode",
375
495
  "command -v claude",
376
496
  "command -v codex",
377
- "command -v gemini",
378
497
  "command -v agent",
379
498
  "command -v cursor-agent",
380
499
  "command -v amp",
381
- "command -v droid",
382
- "command -v pi"
500
+ "command -v droid"
383
501
  ].join(" && "));
384
502
  await runSetup(builder, [
385
503
  `sudo mkdir -p ${shell(RUNTIME_DIR)}`,
@@ -389,19 +507,10 @@ function createVercelSandboxDriver(options) {
389
507
  `npm install ${runtimePackage(expectedRuntimeVersion)} --min-release-age=2`,
390
508
  `node -e "const fs=require('fs');const actual=JSON.parse(fs.readFileSync('${RUNTIME_DIR}/node_modules/@claxedo/workspace-runtime/package.json','utf8')).version;if(actual!=='${expectedRuntimeVersion}'){throw new Error('workspace-runtime snapshot package version mismatch: expected ${expectedRuntimeVersion}, got '+actual)}"`,
391
509
  "test -x node_modules/.bin/workspace-runtime",
392
- "test -x node_modules/.bin/claude-agent-acp",
393
- "test -x node_modules/.bin/codex-acp",
394
510
  `sudo ln -sf ${RUNTIME_DIR}/node_modules/.bin/workspace-runtime /usr/local/bin/workspace-runtime`,
395
- `sudo ln -sf ${RUNTIME_DIR}/node_modules/.bin/claude-agent-acp /usr/local/bin/claude-agent-acp`,
396
- `sudo ln -sf ${RUNTIME_DIR}/node_modules/.bin/codex-acp /usr/local/bin/codex-acp`,
397
- "command -v workspace-runtime",
398
- "command -v claude-agent-acp",
399
- "command -v codex-acp"
511
+ "command -v workspace-runtime"
400
512
  ].join(" && "));
401
- const snapshot = await builder.snapshot({
402
- expiration: 0,
403
- signal: AbortSignal.timeout(snapshotBuildTimeoutMs)
404
- });
513
+ const snapshot = await withDeadlineSignal(snapshotBuildTimeoutMs, (signal) => builder.snapshot({ expiration: 0, signal }));
405
514
  cache.set(cacheKey, snapshot.snapshotId);
406
515
  cacheState.expires.set(cacheKey, now() + SNAPSHOT_CACHE_TTL_MS);
407
516
  return snapshot.snapshotId;
@@ -422,14 +531,14 @@ function createVercelSandboxDriver(options) {
422
531
  }
423
532
  async function startRuntime(sandbox, input, env) {
424
533
  const directory = workspaceDirectory(input);
425
- await sandbox.runCommand({
534
+ await withDeadlineSignal(operationTimeoutMs, (signal) => sandbox.runCommand({
426
535
  cmd: "bash",
427
536
  args: ["-lc", `mkdir -p ${shell(directory)}; cd ${shell(directory)}; exec ${runtimeCommand}`],
428
537
  cwd: "/vercel/sandbox",
429
538
  env,
430
539
  detached: true,
431
- signal: AbortSignal.timeout(operationTimeoutMs)
432
- });
540
+ signal
541
+ }));
433
542
  }
434
543
  async function readyTarget(input, sandbox, hostId) {
435
544
  const url = sandbox.domain(runtimePort);
@@ -450,10 +559,13 @@ function createVercelSandboxDriver(options) {
450
559
  const hostId = input.hostId ?? nameFor(input.workspaceId);
451
560
  const env = {
452
561
  ...bootEnv(input, hostId),
453
- ...await options.env?.(input, { id: hostId })
562
+ ...await options.env?.(input, { id: hostId }),
563
+ // Last, so no caller-supplied variable of the same name can stand in for
564
+ // a placeholder the firewall transform overwrites.
565
+ ...brokeredPlaceholderEnv(input.secrets)
454
566
  };
455
567
  const createPolicy = network(input);
456
- const sandbox = await sandboxFactory.create({
568
+ const sandbox = await (await resolveFactory()).create({
457
569
  ...credentials(),
458
570
  source: await source(input),
459
571
  ports: [runtimePort],
@@ -464,17 +576,17 @@ function createVercelSandboxDriver(options) {
464
576
  if (transientDriverError(err)) return void 0;
465
577
  throw err;
466
578
  });
467
- if (!sandbox) return { provisioning: true, retryAfterMs: 2e3 };
579
+ if (!sandbox) return { provisioning: true, retryAfterMs: TRANSIENT_DRIVER_RETRY_MS };
580
+ await input.onImageReady?.();
468
581
  if (input.secrets?.length) {
469
- await sandbox.updateNetworkPolicy(vercelBrokeredNetworkPolicy(input.secrets, createPolicy), {
470
- signal: AbortSignal.timeout(operationTimeoutMs)
471
- });
582
+ const secrets = input.secrets;
583
+ await withDeadlineSignal(operationTimeoutMs, (signal) => sandbox.updateNetworkPolicy(vercelBrokeredNetworkPolicy(secrets, createPolicy), { signal }));
472
584
  }
473
585
  await startRuntime(sandbox, input, env);
474
586
  return readyTarget(input, sandbox, hostId);
475
587
  }
476
588
  async function sandboxById(target) {
477
- return sandboxFactory.get({
589
+ return (await resolveFactory()).get({
478
590
  ...credentials(),
479
591
  sandboxId: target.sandboxId
480
592
  });
@@ -493,7 +605,7 @@ function createVercelSandboxDriver(options) {
493
605
  ensureHost,
494
606
  resumeHost: (input) => ensureHost(input.ensure),
495
607
  async touch(target) {
496
- await (await sandboxById(target)).extendTimeout(keepAliveMs).catch(() => void 0);
608
+ await (await sandboxById(target)).extendTimeout(keepAliveMs);
497
609
  },
498
610
  async suspend(target) {
499
611
  await (await sandboxById(target)).stop({ blocking: false });
@@ -505,10 +617,8 @@ function createVercelSandboxDriver(options) {
505
617
  await (await sandboxById(target)).stop({ blocking: true });
506
618
  },
507
619
  async snapshot(target) {
508
- const snapshot = await (await sandboxById(target)).snapshot({
509
- expiration: 0,
510
- signal: AbortSignal.timeout(snapshotBuildTimeoutMs)
511
- });
620
+ const sandbox = await sandboxById(target);
621
+ const snapshot = await withDeadlineSignal(snapshotBuildTimeoutMs, (signal) => sandbox.snapshot({ expiration: 0, signal }));
512
622
  return { snapshotId: snapshot.snapshotId };
513
623
  }
514
624
  };