@claxedo/sandbox-manager 0.9.0 → 0.10.1

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 (67) hide show
  1. package/README.md +233 -26
  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 +42 -7
  9. package/dist/driver-catalog.mjs +115 -161
  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 +3 -3
  17. package/dist/drivers/cloudflare.mjs +208 -147
  18. package/dist/drivers/docker.d.ts +4 -3
  19. package/dist/drivers/docker.mjs +154 -126
  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 +2 -2
  23. package/dist/drivers/modal.mjs +121 -111
  24. package/dist/drivers/transient-error.d.ts +2 -0
  25. package/dist/drivers/vercel.d.ts +3 -2
  26. package/dist/drivers/vercel.mjs +151 -115
  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 +9 -2
  31. package/dist/hosted-network-policy.mjs +218 -1
  32. package/dist/image-name.d.ts +12 -2
  33. package/dist/image-name.mjs +14 -5
  34. package/dist/image.d.ts +1 -20
  35. package/dist/image.mjs +11 -88
  36. package/dist/index.d.ts +8 -578
  37. package/dist/index.mjs +1379 -320
  38. package/dist/lease-failure.d.ts +173 -0
  39. package/dist/lease-lifecycle.d.ts +65 -0
  40. package/dist/lease-retry-wait.d.ts +8 -0
  41. package/dist/lease-start.d.ts +21 -0
  42. package/dist/lease-types.d.ts +4 -3
  43. package/dist/manager.d.ts +2 -0
  44. package/dist/runtime-env.d.ts +4 -2
  45. package/dist/runtime-env.mjs +37 -2
  46. package/dist/runtime-snapshot.d.ts +49 -0
  47. package/dist/runtime-version.d.ts +9 -0
  48. package/dist/runtime-version.mjs +2 -0
  49. package/dist/service-url-exposure.d.ts +0 -1
  50. package/dist/service-url-exposure.mjs +0 -9
  51. package/dist/start-fresh.d.ts +17 -0
  52. package/dist/start-telemetry.d.ts +37 -0
  53. package/dist/stores/memory.d.ts +1 -1
  54. package/dist/stores/memory.mjs +63 -52
  55. package/package.json +26 -42
  56. package/dist/daytona-allow-list.d.ts +0 -16
  57. package/dist/daytona-allow-list.mjs +0 -11
  58. package/dist/drivers/box.d.ts +0 -52
  59. package/dist/drivers/box.mjs +0 -574
  60. package/dist/drivers/daytona.d.ts +0 -163
  61. package/dist/drivers/daytona.mjs +0 -683
  62. package/dist/drivers/exe.d.ts +0 -26
  63. package/dist/drivers/exe.mjs +0 -492
  64. package/dist/drivers/fetch-bridge.d.ts +0 -10
  65. package/dist/drivers/fetch-bridge.mjs +0 -126
  66. package/dist/lease-policy.d.ts +0 -52
  67. package/dist/lease-policy.mjs +0 -290
@@ -1,33 +1,4 @@
1
- // src/constants.ts
2
- var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
3
-
4
- // src/hosted-network-policy.ts
5
- var SANDBOX_MODEL_PROVIDER_HOSTS = [
6
- "api.anthropic.com",
7
- "api.openai.com",
8
- "generativelanguage.googleapis.com",
9
- "openrouter.ai",
10
- // Model catalog + engine config, fetched on runner startup.
11
- "models.dev",
12
- "opencode.ai"
13
- ];
14
- var SANDBOX_PACKAGE_REGISTRY_HOSTS = [
15
- "registry.npmjs.org",
16
- "nodejs.org",
17
- "pypi.org",
18
- "files.pythonhosted.org",
19
- "crates.io",
20
- "static.crates.io",
21
- "index.crates.io",
22
- "proxy.golang.org",
23
- "sum.golang.org"
24
- ];
25
- var SANDBOX_BASELINE_EGRESS_HOSTS = [
26
- ...SANDBOX_MODEL_PROVIDER_HOSTS,
27
- ...SANDBOX_PACKAGE_REGISTRY_HOSTS
28
- ];
29
-
30
- // src/index.ts
1
+ // src/egress-policy.ts
31
2
  function brokeredSecretPlaceholder(name) {
32
3
  return `claxedo-broker:${name}`;
33
4
  }
@@ -36,13 +7,43 @@ function brokeredPlaceholderEnv(secrets) {
36
7
  }
37
8
 
38
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
39
  function workspaceRuntimeBootEnv(input) {
40
+ assertWorkspaceRuntimeIdentityEnv(input.env);
40
41
  const env = {
41
42
  ...workspaceRuntimeTargetEnv(input),
42
43
  ...workspaceRuntimeSourceEnv(input),
43
44
  ...input.env
44
45
  };
45
- if (input.runner) env.WORKSPACE_RUNTIME_RUNNER = input.runner;
46
+ if (input.nativeHarness) env.WORKSPACE_RUNTIME_NATIVE_HARNESS = input.nativeHarness;
46
47
  if (input.controlEnv?.relayJwksUrl) env.WORKSPACE_RUNTIME_RELAY_JWKS_URL = input.controlEnv.relayJwksUrl;
47
48
  if (input.controlEnv?.relayVerifyPem) env.WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM = input.controlEnv.relayVerifyPem;
48
49
  if (input.controlEnv?.managementJwksUrl) env.WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL = input.controlEnv.managementJwksUrl;
@@ -78,13 +79,43 @@ function shell(input) {
78
79
  return `'${input.replace(/'/g, `'"'"'`)}'`;
79
80
  }
80
81
 
82
+ // src/constants.ts
83
+ var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
84
+
81
85
  // src/defaults.ts
82
86
  var RUNTIME_DIR = "/opt/workspace-runtime";
83
87
 
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
+
84
117
  // ../sandbox-contract/dist/index.mjs
85
118
  var sandboxDriverCredentialFields = {
86
- exe: [{ key: "api_token", label: "API Token", secret: true }],
87
- daytona: [{ key: "api_key", label: "API Key", secret: true }],
88
119
  modal: [
89
120
  { key: "token_id", label: "Token ID", secret: true },
90
121
  { key: "token_secret", label: "Token Secret", secret: true }
@@ -98,68 +129,26 @@ var sandboxDriverCredentialFields = {
98
129
  { key: "api_token", label: "API Token", secret: true },
99
130
  { key: "worker_url", label: "Worker URL" }
100
131
  ],
101
- box: [{ key: "api_key", label: "API Key", secret: true }],
132
+ boat: [{ key: "api_key", label: "API Key", secret: true }],
102
133
  docker: [{ key: "image", label: "Image" }]
103
134
  };
104
135
  var sandboxDriverLabels = {
105
- exe: "exe.dev",
106
- daytona: "Daytona",
107
136
  modal: "Modal",
108
137
  vercel: "Vercel",
109
138
  cloudflare: "Cloudflare",
110
- box: "Box",
139
+ boat: "Boat",
111
140
  docker: "Docker"
112
141
  };
113
142
 
114
143
  // src/driver-catalog.ts
115
144
  var sandboxDriverCatalog = {
116
- exe: {
117
- id: "exe",
118
- label: sandboxDriverLabels.exe,
119
- credentialFields: sandboxDriverCredentialFields.exe,
120
- metadata: {
121
- driverRunsIn: ["worker", "node"],
122
- hostStopBehavior: "not-supported",
123
- hostResumeBehavior: "same-host",
124
- targetAccess: "relay",
125
- secretBrokering: "none",
126
- egressControl: "none",
127
- persistence: {
128
- resume: "same-sandbox",
129
- capture: "none",
130
- clone: true,
131
- captureSource: "not-applicable",
132
- retention: "provider-managed",
133
- restoreMount: "same-resource"
134
- }
135
- }
136
- },
137
- daytona: {
138
- id: "daytona",
139
- label: sandboxDriverLabels.daytona,
140
- credentialFields: sandboxDriverCredentialFields.daytona,
141
- metadata: {
142
- driverRunsIn: ["worker", "node"],
143
- hostStopBehavior: "suspends-host",
144
- hostResumeBehavior: "same-host",
145
- targetAccess: "relay",
146
- secretBrokering: "native",
147
- // The driver translates names to domainAllowList, or addresses to networkAllowList.
148
- egressControl: "hosts-and-cidrs",
149
- persistence: {
150
- resume: "same-sandbox",
151
- capture: "filesystem",
152
- clone: false,
153
- captureSource: "preserved",
154
- retention: "provider-managed",
155
- restoreMount: "new-resource"
156
- }
157
- }
158
- },
159
145
  modal: {
160
146
  id: "modal",
161
147
  label: sandboxDriverLabels.modal,
162
148
  credentialFields: sandboxDriverCredentialFields.modal,
149
+ runtimeNetwork: { controlPlane: "direct", relay: "configured" },
150
+ runtimeEnv: "callback",
151
+ preparedImage: true,
163
152
  metadata: {
164
153
  driverRunsIn: ["node"],
165
154
  hostStopBehavior: "terminates-host",
@@ -182,6 +171,9 @@ var sandboxDriverCatalog = {
182
171
  id: "vercel",
183
172
  label: sandboxDriverLabels.vercel,
184
173
  credentialFields: sandboxDriverCredentialFields.vercel,
174
+ runtimeNetwork: { controlPlane: "direct", relay: "configured" },
175
+ runtimeEnv: "callback",
176
+ preparedImage: false,
185
177
  metadata: {
186
178
  driverRunsIn: ["node"],
187
179
  hostStopBehavior: "terminates-host",
@@ -204,30 +196,40 @@ var sandboxDriverCatalog = {
204
196
  id: "cloudflare",
205
197
  label: sandboxDriverLabels.cloudflare,
206
198
  credentialFields: sandboxDriverCredentialFields.cloudflare,
199
+ runtimeNetwork: { controlPlane: "direct", relay: "configured" },
200
+ runtimeEnv: "callback",
201
+ preparedImage: false,
202
+ billing: cloudflareBilling,
207
203
  metadata: {
208
204
  driverRunsIn: ["worker"],
209
- hostStopBehavior: "not-supported",
205
+ hostStopBehavior: "terminates-host",
210
206
  hostResumeBehavior: "same-host",
207
+ hostWatchesIdle: true,
211
208
  targetAccess: "relay",
212
209
  secretBrokering: "native",
213
210
  // Credential handlers do not restrict unrelated destinations.
214
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.
215
213
  persistence: {
216
214
  resume: "replacement-restore",
217
- capture: "directories",
215
+ capture: "filesystem",
218
216
  clone: false,
219
217
  captureSource: "preserved",
220
218
  retention: "provider-managed",
221
- restoreMount: "copy-on-write"
219
+ restoreMount: "new-resource"
222
220
  }
223
221
  }
224
222
  },
225
- box: {
226
- id: "box",
227
- label: sandboxDriverLabels.box,
228
- credentialFields: sandboxDriverCredentialFields.box,
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,
229
231
  metadata: {
230
- driverRunsIn: ["node"],
232
+ driverRunsIn: ["worker", "node"],
231
233
  hostStopBehavior: "suspends-host",
232
234
  hostResumeBehavior: "same-host",
233
235
  targetAccess: "relay",
@@ -240,13 +242,17 @@ var sandboxDriverCatalog = {
240
242
  captureSource: "not-applicable",
241
243
  retention: "provider-managed",
242
244
  restoreMount: "same-resource"
243
- }
245
+ },
246
+ machineClasses: ["small", "default", "large"]
244
247
  }
245
248
  },
246
249
  docker: {
247
250
  id: "docker",
248
251
  label: sandboxDriverLabels.docker,
249
252
  credentialFields: sandboxDriverCredentialFields.docker,
253
+ runtimeNetwork: { controlPlane: "docker-host", relay: "configured" },
254
+ runtimeEnv: "callback",
255
+ preparedImage: true,
250
256
  metadata: {
251
257
  driverRunsIn: ["local"],
252
258
  hostStopBehavior: "terminates-host",
@@ -287,7 +293,9 @@ function driverErrorSignals(err) {
287
293
  const shaped = isRecord(err) ? err : {};
288
294
  const nested = isRecord(shaped.response) ? shaped.response : {};
289
295
  return {
290
- status: num(nested.status) ?? num(shaped.status),
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),
291
299
  text: `${textOf(shaped.code)} ${textOf(shaped.name)} ${textOf(shaped.message)}`.toLowerCase()
292
300
  };
293
301
  }
@@ -296,9 +304,28 @@ function isTransientDriverError(err, markers) {
296
304
  if (status !== void 0 && status >= 500) return true;
297
305
  return markers.some((marker) => text.includes(marker));
298
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
+ }
299
319
 
300
320
  // src/drivers/vercel.ts
301
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"`;
302
329
  var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
303
330
  var DEFAULT_WORKSPACE_DIR = "/workspace";
304
331
  var DEFAULT_TIMEOUT_MS = 30 * 60 * 1e3;
@@ -346,8 +373,14 @@ function vercelBrokeredNetworkPolicy(secrets, base) {
346
373
  for (const host of secret.hosts) {
347
374
  const rules = allow[host] ??= [];
348
375
  for (const prefix of methods.length ? pathPrefixes : []) {
376
+ const path = credentialPathPattern(prefix);
377
+ if (path === void 0) continue;
349
378
  rules.push({
350
- match: { path: { startsWith: prefix }, method: methods },
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
+ },
351
384
  transform: [{ headers: { [secret.header]: value } }]
352
385
  });
353
386
  }
@@ -356,6 +389,17 @@ function vercelBrokeredNetworkPolicy(secrets, base) {
356
389
  const subnets = typeof base === "object" && base?.subnets ? { subnets: base.subnets } : {};
357
390
  return { allow, ...subnets };
358
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
+ }
359
403
  function baseAllowEntries(base) {
360
404
  if (base === void 0 || base === "allow-all") return [["*", []]];
361
405
  if (base === "deny-all") return [];
@@ -409,14 +453,12 @@ function createVercelSandboxDriver(options) {
409
453
  host: "0.0.0.0",
410
454
  source: input.source,
411
455
  env: input.env,
412
- runner: options.runner,
456
+ nativeHarness: options.nativeHarness,
413
457
  controlEnv: options.controlEnv
414
458
  });
415
459
  }
416
460
  async function runSetup(sandbox, command) {
417
- await sandbox.runCommand("bash", ["-c", command], {
418
- signal: AbortSignal.timeout(snapshotBuildTimeoutMs)
419
- });
461
+ await withDeadlineSignal(snapshotBuildTimeoutMs, (signal) => sandbox.runCommand("bash", ["-c", command], { signal }));
420
462
  }
421
463
  async function ensureSnapshot() {
422
464
  if (options.baseSnapshotId) return options.baseSnapshotId;
@@ -440,7 +482,8 @@ function createVercelSandboxDriver(options) {
440
482
  });
441
483
  await runSetup(builder, "sudo dnf install -y git make gcc-c++ python3 > /dev/null 2>&1");
442
484
  await runSetup(builder, [
443
- "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",
444
487
  "curl https://cursor.com/install -fsS | bash",
445
488
  "curl -fsSL https://ampcode.com/install.sh | bash",
446
489
  "curl -fsSL https://app.factory.ai/cli | sh",
@@ -451,12 +494,10 @@ function createVercelSandboxDriver(options) {
451
494
  "command -v opencode",
452
495
  "command -v claude",
453
496
  "command -v codex",
454
- "command -v gemini",
455
497
  "command -v agent",
456
498
  "command -v cursor-agent",
457
499
  "command -v amp",
458
- "command -v droid",
459
- "command -v pi"
500
+ "command -v droid"
460
501
  ].join(" && "));
461
502
  await runSetup(builder, [
462
503
  `sudo mkdir -p ${shell(RUNTIME_DIR)}`,
@@ -469,10 +510,7 @@ function createVercelSandboxDriver(options) {
469
510
  `sudo ln -sf ${RUNTIME_DIR}/node_modules/.bin/workspace-runtime /usr/local/bin/workspace-runtime`,
470
511
  "command -v workspace-runtime"
471
512
  ].join(" && "));
472
- const snapshot = await builder.snapshot({
473
- expiration: 0,
474
- signal: AbortSignal.timeout(snapshotBuildTimeoutMs)
475
- });
513
+ const snapshot = await withDeadlineSignal(snapshotBuildTimeoutMs, (signal) => builder.snapshot({ expiration: 0, signal }));
476
514
  cache.set(cacheKey, snapshot.snapshotId);
477
515
  cacheState.expires.set(cacheKey, now() + SNAPSHOT_CACHE_TTL_MS);
478
516
  return snapshot.snapshotId;
@@ -493,14 +531,14 @@ function createVercelSandboxDriver(options) {
493
531
  }
494
532
  async function startRuntime(sandbox, input, env) {
495
533
  const directory = workspaceDirectory(input);
496
- await sandbox.runCommand({
534
+ await withDeadlineSignal(operationTimeoutMs, (signal) => sandbox.runCommand({
497
535
  cmd: "bash",
498
536
  args: ["-lc", `mkdir -p ${shell(directory)}; cd ${shell(directory)}; exec ${runtimeCommand}`],
499
537
  cwd: "/vercel/sandbox",
500
538
  env,
501
539
  detached: true,
502
- signal: AbortSignal.timeout(operationTimeoutMs)
503
- });
540
+ signal
541
+ }));
504
542
  }
505
543
  async function readyTarget(input, sandbox, hostId) {
506
544
  const url = sandbox.domain(runtimePort);
@@ -538,11 +576,11 @@ function createVercelSandboxDriver(options) {
538
576
  if (transientDriverError(err)) return void 0;
539
577
  throw err;
540
578
  });
541
- if (!sandbox) return { provisioning: true, retryAfterMs: 2e3 };
579
+ if (!sandbox) return { provisioning: true, retryAfterMs: TRANSIENT_DRIVER_RETRY_MS };
580
+ await input.onImageReady?.();
542
581
  if (input.secrets?.length) {
543
- await sandbox.updateNetworkPolicy(vercelBrokeredNetworkPolicy(input.secrets, createPolicy), {
544
- signal: AbortSignal.timeout(operationTimeoutMs)
545
- });
582
+ const secrets = input.secrets;
583
+ await withDeadlineSignal(operationTimeoutMs, (signal) => sandbox.updateNetworkPolicy(vercelBrokeredNetworkPolicy(secrets, createPolicy), { signal }));
546
584
  }
547
585
  await startRuntime(sandbox, input, env);
548
586
  return readyTarget(input, sandbox, hostId);
@@ -567,7 +605,7 @@ function createVercelSandboxDriver(options) {
567
605
  ensureHost,
568
606
  resumeHost: (input) => ensureHost(input.ensure),
569
607
  async touch(target) {
570
- await (await sandboxById(target)).extendTimeout(keepAliveMs).catch(() => void 0);
608
+ await (await sandboxById(target)).extendTimeout(keepAliveMs);
571
609
  },
572
610
  async suspend(target) {
573
611
  await (await sandboxById(target)).stop({ blocking: false });
@@ -579,10 +617,8 @@ function createVercelSandboxDriver(options) {
579
617
  await (await sandboxById(target)).stop({ blocking: true });
580
618
  },
581
619
  async snapshot(target) {
582
- const snapshot = await (await sandboxById(target)).snapshot({
583
- expiration: 0,
584
- signal: AbortSignal.timeout(snapshotBuildTimeoutMs)
585
- });
620
+ const sandbox = await sandboxById(target);
621
+ const snapshot = await withDeadlineSignal(snapshotBuildTimeoutMs, (signal) => sandbox.snapshot({ expiration: 0, signal }));
586
622
  return { snapshotId: snapshot.snapshotId };
587
623
  }
588
624
  };
@@ -0,0 +1,134 @@
1
+ export type SandboxNetworkPolicy = {
2
+ mode: "allow-all" | "restricted";
3
+ hosts?: string[];
4
+ cidrs?: string[];
5
+ };
6
+ /** Brokered destinations are the minimum egress needed to use the credential. */
7
+ export declare function sandboxNetworkPolicyWithBrokeredHosts(policy: SandboxNetworkPolicy, secrets: readonly SandboxBrokeredSecret[] | undefined): SandboxNetworkPolicy;
8
+ /** @see SandboxDriverMetadata.egressControl */
9
+ export type SandboxEgressControl = "none" | "hosts" | "hosts-and-cidrs";
10
+ /**
11
+ * What the manager does with a caller's `net` for a given driver: enforce
12
+ * where the driver can, and say so loudly rather than refuse where it cannot.
13
+ *
14
+ * - `"enforce"` — the driver gets the policy verbatim and contains the
15
+ * sandbox. Also the answer when the caller asked for no containment
16
+ * (absent / `allow-all`), which is nothing to enforce.
17
+ * - `"withhold"` — the driver declares `egressControl: "none"`, so there is no
18
+ * containment to be had. The policy is NOT handed down: half the `"none"`
19
+ * drivers throw on a restricted policy (docker, modal, boat) and the
20
+ * other half silently drop it (cloudflare, the fetch bridge). Withholding at
21
+ * the manager means the throwing drivers never see one — their throws stay
22
+ * in place as their own last line of defence — and the silently-dropping
23
+ * ones stop pretending. The sandbox comes up with UNRESTRICTED egress, which
24
+ * is a real exposure, so the manager warns (see `onEgressUnenforced`).
25
+ * - `"refuse"` — the driver HAS egress control but cannot express this
26
+ * particular policy's encoding (a hosts-only driver handed an address-only
27
+ * allowlist). Deliberately still fail-closed: this is a composition mistake
28
+ * on an otherwise-enforcing driver, and degrading it to "withhold" would
29
+ * weaken the enforcing path. `hostedSandboxNetworkPolicy` only ever emits
30
+ * hosts, so no production path reaches it.
31
+ *
32
+ * Pure, like `validateSandboxPersistenceCapabilities`, so the manager, the
33
+ * composition layer, and tests can all ask the same question.
34
+ */
35
+ export type SandboxEgressDisposition = {
36
+ action: "enforce";
37
+ } | {
38
+ action: "withhold";
39
+ reason: "sandbox_egress_uncontained";
40
+ } | {
41
+ action: "refuse";
42
+ reason: "sandbox_egress_policy_unenforceable";
43
+ };
44
+ /** @see SandboxEgressDisposition */
45
+ export declare function sandboxEgressDisposition(control: SandboxEgressControl, net: SandboxNetworkPolicy | undefined): SandboxEgressDisposition;
46
+ /**
47
+ * Emitted whenever a restricted egress policy is withheld because the composed
48
+ * driver declares no egress control: an unrestricted sandbox that nobody was
49
+ * told about is the exposure, so the degrade is never silent.
50
+ *
51
+ * Two phases, because a per-create line is easy to miss in request logs:
52
+ * - `"composition"` — once per `createSandboxManager` per driver, at boot.
53
+ * - `"ensure"` — every time a caller's policy is actually withheld, naming the
54
+ * workspace and the allowlist that did not take effect.
55
+ */
56
+ export type SandboxEgressUnenforcedEvent = {
57
+ phase: "composition" | "ensure";
58
+ reason: "sandbox_egress_uncontained";
59
+ /** `SandboxDriver.id` of the composed driver. */
60
+ driver: string;
61
+ /** Always `"none"` today — carried so a sink can group without re-deriving. */
62
+ egressControl: SandboxEgressControl;
63
+ /** Ready-to-log sentence. Sinks that only forward text can use this alone. */
64
+ message: string;
65
+ /** Present on `"ensure"`. */
66
+ workspaceId?: string;
67
+ /** The start's placement labels. Present on `"ensure"`. */
68
+ labels?: Record<string, string>;
69
+ /** The allowlist the caller asked for and did NOT get. Present on `"ensure"`. */
70
+ requested?: SandboxNetworkPolicy;
71
+ };
72
+ /**
73
+ * A credential the SANDBOX must be able to USE for outbound requests but must
74
+ * NEVER be able to READ. Unlike `env` (plaintext, readable — reserved for
75
+ * credentials the agent is trusted with, e.g. the user's own model
76
+ * subscription), a brokered secret's `value` never enters the sandbox: the
77
+ * provider injects it on egress to `hosts` only. Non-model credentials
78
+ * (connection tokens, deploy tokens) belong here.
79
+ *
80
+ * Guarantees enforced by the manager: brokered secrets are never written to
81
+ * labels, never logged, and never captured in a driver snapshot. A driver
82
+ * whose `metadata.secretBrokering` is not `"native"` cannot honor them and the
83
+ * manager refuses to provision (fail-closed).
84
+ */
85
+ export type SandboxBrokeredSecret = {
86
+ /** Env var name the sandbox references. */
87
+ name: string;
88
+ /** The secret material. Never enters the sandbox in plaintext. */
89
+ value: string;
90
+ /** Egress allowlist: the only hosts for which the value is substituted/injected. */
91
+ hosts: string[];
92
+ /**
93
+ * The HTTP header the value is injected as on egress to `hosts`. Every
94
+ * native driver needs one and refuses a secret without it.
95
+ */
96
+ header?: string;
97
+ /**
98
+ * The authentication scheme `header` carries, when it has one (`Bearer`).
99
+ *
100
+ * A driver that WRITES the whole header composes `"<scheme> <value>"`; one
101
+ * that SUBSTITUTES a placeholder the sandbox already wrote after the scheme
102
+ * ignores it, because the scheme is in the request before the value is.
103
+ */
104
+ scheme?: string;
105
+ /**
106
+ * The methods and path prefixes the credential may be attached to, on top of
107
+ * `hosts`. A vendor host serves far more than the routes a turn needs —
108
+ * api.anthropic.com also answers the organization-admin API — and everything
109
+ * sharing the sandbox reaches the same host.
110
+ *
111
+ * Optional so a producer that has not been taught to state a policy still
112
+ * type-checks, and fails closed instead: a driver that can express them
113
+ * attaches the credential only within them, so an absent or empty policy
114
+ * names a host and nothing else, and nothing is spendable at a host alone. A
115
+ * driver whose provider cannot express them documents that it drops them,
116
+ * and the host allowlist is all the containment there is.
117
+ */
118
+ methods?: readonly string[];
119
+ pathPrefixes?: readonly string[];
120
+ };
121
+ /**
122
+ * What the sandbox presents so a header-injecting driver recognizes the request
123
+ * as one asking for that secret.
124
+ *
125
+ * Carries no authority: the value is attached at the edge, keyed by the host
126
+ * and this string, and the string itself is never accepted by a vendor. The
127
+ * Cloudflare sandbox Worker is deployed on its own and cannot depend on this
128
+ * package, so it matches on its own copy of the prefix; a driver minting
129
+ * anything else sends every brokered request upstream bare, which is what
130
+ * `brokered-placeholder.test.ts` exists to catch.
131
+ */
132
+ export declare function brokeredSecretPlaceholder(name: string): string;
133
+ /** The environment a header-injecting driver must add so the sandbox can present each secret. */
134
+ export declare function brokeredPlaceholderEnv(secrets: readonly SandboxBrokeredSecret[] | undefined): Record<string, string>;
@@ -0,0 +1,22 @@
1
+ import type { SandboxEgressControl, SandboxEgressUnenforcedEvent, SandboxNetworkPolicy } from "./egress-policy";
2
+ /**
3
+ * The two halves of the warning that a driver runs sandboxes with unrestricted
4
+ * egress. The composition half is raised here, once per driver per isolate: a
5
+ * per-create line lands in request logs and is easy to miss, this one lands
6
+ * wherever the process starts, so an operator learns of it BEFORE the first
7
+ * workspace exists. `withheld` is the start half, raised once per driver per
8
+ * isolate from the first start whose policy the driver could not enforce.
9
+ */
10
+ export declare function createEgressWarnings(input: {
11
+ driver: {
12
+ id: string;
13
+ egressControl: SandboxEgressControl;
14
+ };
15
+ sink?: (event: SandboxEgressUnenforcedEvent) => void;
16
+ }): {
17
+ withheld(start: {
18
+ workspaceId: string;
19
+ labels?: Record<string, string>;
20
+ requested: SandboxNetworkPolicy;
21
+ }): void;
22
+ };
@@ -0,0 +1,14 @@
1
+ import { type SandboxDriver, type SandboxGarbageCollectResult, type SandboxLeaseStore } from "./contract";
2
+ /**
3
+ * Destroys every sandbox the driver lists under `appLabel` that no lease owns.
4
+ * The lease store is authoritative for which provider resource a workspace
5
+ * owns, so the sweep reads the provider first and the leases second: a driver
6
+ * that cannot enumerate answers "we could not look", never "nothing is
7
+ * orphaned", because an empty listing reaching the sweep would make every live
8
+ * sandbox look like an orphan and destroy the fleet.
9
+ */
10
+ export declare function sweepSandboxes(input: {
11
+ driver: SandboxDriver;
12
+ leaseStore: SandboxLeaseStore;
13
+ appLabel: string;
14
+ }): Promise<SandboxGarbageCollectResult>;
@@ -36,7 +36,8 @@
36
36
  * the only kind the Worker can compute, which is why `egressControl` treats
37
37
  * name filtering as the capability that qualifies a driver for hosted use.
38
38
  */
39
- import type { SandboxNetworkPolicy, SandboxSource } from ".";
39
+ import type { SandboxSource } from "./contract";
40
+ import type { SandboxNetworkPolicy } from "./egress-policy";
40
41
  /**
41
42
  * Model-provider endpoints. Derived from the harness→provider groups in
42
43
  * claxedo-server `network/types.ts` (`PROVIDER_TO_GROUP`): claude-sdk →
@@ -58,9 +59,15 @@ export declare const SANDBOX_BASELINE_EGRESS_HOSTS: readonly ["api.anthropic.com
58
59
  * The git host this workspace clones from.
59
60
  *
60
61
  * Scoped to the ONE repository host in the request rather than to a forge
61
- * allowlist, matching how `authenticatedGitHubCloneSource` scopes the clone
62
+ * allowlist, matching how `githubCloneSecret` scopes the clone
62
63
  * credential it brokers (`hosts: ["github.com"]`) — the credential and the
63
64
  * network policy should describe the same reachable surface.
65
+ *
66
+ * The host is caller-selected, so only a public spelling earns an allowlist
67
+ * entry: `169.254.169.254` or `localhost` named as a repoUrl would otherwise
68
+ * open the sandbox's egress to the provisioning network's own addresses. A
69
+ * DNS name cannot be seen through here (no resolver in this module); a name
70
+ * that hides a private answer is clone admission's refusal, not this one's.
64
71
  */
65
72
  export declare function sandboxSourceHost(source: SandboxSource | undefined): string | undefined;
66
73
  export type HostedSandboxNetworkPolicyInput = {