@claxedo/sandbox-manager 0.6.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +60 -15
  2. package/dist/checkpoint-manager.d.ts +47 -0
  3. package/dist/checkpoint-manager.mjs +116 -0
  4. package/dist/daytona-allow-list.d.ts +11 -0
  5. package/dist/daytona-allow-list.mjs +5 -1
  6. package/dist/driver-catalog.d.ts +5 -52
  7. package/dist/driver-catalog.mjs +263 -121
  8. package/dist/drivers/box.mjs +309 -39
  9. package/dist/drivers/cloudflare.d.ts +18 -7
  10. package/dist/drivers/cloudflare.mjs +423 -34
  11. package/dist/drivers/daytona.d.ts +82 -13
  12. package/dist/drivers/daytona.mjs +495 -45
  13. package/dist/drivers/docker.mjs +284 -38
  14. package/dist/drivers/exe.d.ts +26 -0
  15. package/dist/drivers/exe.mjs +492 -0
  16. package/dist/drivers/fetch-bridge.mjs +29 -5
  17. package/dist/drivers/modal.d.ts +2 -5
  18. package/dist/drivers/modal.mjs +296 -46
  19. package/dist/drivers/transient-error.d.ts +11 -0
  20. package/dist/drivers/vercel.d.ts +27 -11
  21. package/dist/drivers/vercel.mjs +339 -59
  22. package/dist/hosted-network-policy.d.ts +87 -0
  23. package/dist/hosted-network-policy.mjs +61 -0
  24. package/dist/image-name.mjs +34 -0
  25. package/dist/image.d.ts +18 -3
  26. package/dist/image.mjs +47 -32
  27. package/dist/index.d.ts +281 -17
  28. package/dist/index.mjs +393 -26
  29. package/dist/json.d.ts +13 -0
  30. package/dist/lease-policy.d.ts +6 -3
  31. package/dist/lease-policy.mjs +213 -54
  32. package/dist/lease-types.d.ts +17 -3
  33. package/dist/runtime-env.d.ts +12 -1
  34. package/dist/runtime-env.mjs +14 -0
  35. package/dist/runtime-version.mjs +1 -9
  36. package/dist/service-url-exposure.d.ts +1 -2
  37. package/dist/service-url-exposure.mjs +1 -17
  38. package/dist/stores/memory.mjs +39 -4
  39. package/package.json +67 -51
  40. package/dist/drivers/cloudflare-egress.d.ts +0 -40
  41. package/dist/drivers/cloudflare-egress.mjs +0 -113
@@ -1,7 +1,54 @@
1
- // src/drivers/vercel.ts
2
- import { Sandbox } from "@vercel/sandbox";
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
31
+ function brokeredSecretPlaceholder(name) {
32
+ return `claxedo-broker:${name}`;
33
+ }
34
+ function brokeredPlaceholderEnv(secrets) {
35
+ return Object.fromEntries((secrets ?? []).map((secret) => [secret.name, brokeredSecretPlaceholder(secret.name)]));
36
+ }
3
37
 
4
38
  // src/runtime-env.ts
39
+ function workspaceRuntimeBootEnv(input) {
40
+ const env = {
41
+ ...workspaceRuntimeTargetEnv(input),
42
+ ...workspaceRuntimeSourceEnv(input),
43
+ ...input.env
44
+ };
45
+ if (input.runner) env.WORKSPACE_RUNTIME_RUNNER = input.runner;
46
+ if (input.controlEnv?.relayJwksUrl) env.WORKSPACE_RUNTIME_RELAY_JWKS_URL = input.controlEnv.relayJwksUrl;
47
+ if (input.controlEnv?.relayVerifyPem) env.WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM = input.controlEnv.relayVerifyPem;
48
+ if (input.controlEnv?.managementJwksUrl) env.WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL = input.controlEnv.managementJwksUrl;
49
+ if (input.controlEnv?.sessionAuthorityUrl) env.WORKSPACE_RUNTIME_SESSION_AUTHORITY_URL = input.controlEnv.sessionAuthorityUrl;
50
+ return env;
51
+ }
5
52
  function workspaceRuntimeTargetEnv(input) {
6
53
  return {
7
54
  WORKSPACE_RUNTIME_WORKSPACE_ID: input.workspaceId,
@@ -22,16 +69,8 @@ function workspaceRuntimeSourceEnv(input) {
22
69
 
23
70
  // src/runtime-version.ts
24
71
  var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
25
- var versionCache;
26
72
  function workspaceRuntimeVersion() {
27
- if (versionCache) return versionCache;
28
- const envVersion = process.env.WORKSPACE_RUNTIME_VERSION;
29
- if (envVersion) {
30
- versionCache = envVersion;
31
- return versionCache;
32
- }
33
- versionCache = DEFAULT_WORKSPACE_RUNTIME_VERSION;
34
- return versionCache;
73
+ return process.env.WORKSPACE_RUNTIME_VERSION || DEFAULT_WORKSPACE_RUNTIME_VERSION;
35
74
  }
36
75
 
37
76
  // src/command.ts
@@ -39,12 +78,225 @@ function shell(input) {
39
78
  return `'${input.replace(/'/g, `'"'"'`)}'`;
40
79
  }
41
80
 
42
- // src/constants.ts
43
- var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
44
-
45
81
  // src/defaults.ts
46
82
  var RUNTIME_DIR = "/opt/workspace-runtime";
47
83
 
84
+ // ../sandbox-contract/dist/index.mjs
85
+ var sandboxDriverCredentialFields = {
86
+ exe: [{ key: "api_token", label: "API Token", secret: true }],
87
+ daytona: [{ key: "api_key", label: "API Key", secret: true }],
88
+ modal: [
89
+ { key: "token_id", label: "Token ID", secret: true },
90
+ { key: "token_secret", label: "Token Secret", secret: true }
91
+ ],
92
+ vercel: [
93
+ { key: "access_token", label: "Access Token", secret: true },
94
+ { key: "team_id", label: "Team ID" },
95
+ { key: "project_id", label: "Project ID" }
96
+ ],
97
+ cloudflare: [
98
+ { key: "api_token", label: "API Token", secret: true },
99
+ { key: "worker_url", label: "Worker URL" }
100
+ ],
101
+ box: [{ key: "api_key", label: "API Key", secret: true }],
102
+ docker: [{ key: "image", label: "Image" }]
103
+ };
104
+ var sandboxDriverLabels = {
105
+ exe: "exe.dev",
106
+ daytona: "Daytona",
107
+ modal: "Modal",
108
+ vercel: "Vercel",
109
+ cloudflare: "Cloudflare",
110
+ box: "Box",
111
+ docker: "Docker"
112
+ };
113
+
114
+ // src/driver-catalog.ts
115
+ 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
+ modal: {
160
+ id: "modal",
161
+ label: sandboxDriverLabels.modal,
162
+ credentialFields: sandboxDriverCredentialFields.modal,
163
+ metadata: {
164
+ driverRunsIn: ["node"],
165
+ hostStopBehavior: "terminates-host",
166
+ hostResumeBehavior: "replacement-host",
167
+ targetAccess: "relay",
168
+ secretBrokering: "none",
169
+ // The driver does not install Modal's domain allowlist or alpha sidecar.
170
+ egressControl: "none",
171
+ persistence: {
172
+ resume: "replacement-restore",
173
+ capture: "filesystem",
174
+ clone: false,
175
+ captureSource: "preserved",
176
+ retention: "provider-managed",
177
+ restoreMount: "new-resource"
178
+ }
179
+ }
180
+ },
181
+ vercel: {
182
+ id: "vercel",
183
+ label: sandboxDriverLabels.vercel,
184
+ credentialFields: sandboxDriverCredentialFields.vercel,
185
+ metadata: {
186
+ driverRunsIn: ["node"],
187
+ hostStopBehavior: "terminates-host",
188
+ hostResumeBehavior: "replacement-host",
189
+ targetAccess: "relay",
190
+ secretBrokering: "native",
191
+ // The driver translates hosts only; the SDK's subnet rules are not wired to net.cidrs.
192
+ egressControl: "hosts",
193
+ persistence: {
194
+ resume: "replacement-restore",
195
+ capture: "filesystem",
196
+ clone: false,
197
+ captureSource: "stopped",
198
+ retention: "explicit",
199
+ restoreMount: "new-resource"
200
+ }
201
+ }
202
+ },
203
+ cloudflare: {
204
+ id: "cloudflare",
205
+ label: sandboxDriverLabels.cloudflare,
206
+ credentialFields: sandboxDriverCredentialFields.cloudflare,
207
+ metadata: {
208
+ driverRunsIn: ["worker"],
209
+ hostStopBehavior: "not-supported",
210
+ hostResumeBehavior: "same-host",
211
+ targetAccess: "relay",
212
+ secretBrokering: "native",
213
+ // Credential handlers do not restrict unrelated destinations.
214
+ egressControl: "none",
215
+ persistence: {
216
+ resume: "replacement-restore",
217
+ capture: "directories",
218
+ clone: false,
219
+ captureSource: "preserved",
220
+ retention: "provider-managed",
221
+ restoreMount: "copy-on-write"
222
+ }
223
+ }
224
+ },
225
+ box: {
226
+ id: "box",
227
+ label: sandboxDriverLabels.box,
228
+ credentialFields: sandboxDriverCredentialFields.box,
229
+ metadata: {
230
+ driverRunsIn: ["node"],
231
+ hostStopBehavior: "suspends-host",
232
+ hostResumeBehavior: "same-host",
233
+ targetAccess: "relay",
234
+ secretBrokering: "none",
235
+ egressControl: "none",
236
+ persistence: {
237
+ resume: "same-sandbox",
238
+ capture: "none",
239
+ clone: false,
240
+ captureSource: "not-applicable",
241
+ retention: "provider-managed",
242
+ restoreMount: "same-resource"
243
+ }
244
+ }
245
+ },
246
+ docker: {
247
+ id: "docker",
248
+ label: sandboxDriverLabels.docker,
249
+ credentialFields: sandboxDriverCredentialFields.docker,
250
+ metadata: {
251
+ driverRunsIn: ["local"],
252
+ hostStopBehavior: "terminates-host",
253
+ hostResumeBehavior: "same-host",
254
+ targetAccess: "loopback",
255
+ secretBrokering: "none",
256
+ egressControl: "none",
257
+ persistence: {
258
+ resume: "replacement-restore",
259
+ capture: "same-resource",
260
+ clone: false,
261
+ captureSource: "preserved",
262
+ retention: "provider-managed",
263
+ restoreMount: "same-resource"
264
+ }
265
+ }
266
+ }
267
+ };
268
+
269
+ // ../claxedo-helpers/dist/guards.mjs
270
+ function isRecord(value) {
271
+ return value !== null && typeof value === "object" && !Array.isArray(value);
272
+ }
273
+ function asRecord(value) {
274
+ return isRecord(value) ? value : void 0;
275
+ }
276
+
277
+ // src/json.ts
278
+ function num(input) {
279
+ return typeof input === "number" ? input : void 0;
280
+ }
281
+
282
+ // src/drivers/transient-error.ts
283
+ function textOf(input) {
284
+ return typeof input === "string" ? input : "";
285
+ }
286
+ function driverErrorSignals(err) {
287
+ const shaped = isRecord(err) ? err : {};
288
+ const nested = isRecord(shaped.response) ? shaped.response : {};
289
+ return {
290
+ status: num(nested.status) ?? num(shaped.status),
291
+ text: `${textOf(shaped.code)} ${textOf(shaped.name)} ${textOf(shaped.message)}`.toLowerCase()
292
+ };
293
+ }
294
+ function isTransientDriverError(err, markers) {
295
+ const { status, text } = driverErrorSignals(err);
296
+ if (status !== void 0 && status >= 500) return true;
297
+ return markers.some((marker) => text.includes(marker));
298
+ }
299
+
48
300
  // src/drivers/vercel.ts
49
301
  var DEFAULT_RUNTIME = "node22";
50
302
  var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
@@ -53,20 +305,22 @@ var DEFAULT_TIMEOUT_MS = 30 * 60 * 1e3;
53
305
  var DEFAULT_SNAPSHOT_BUILD_TIMEOUT_MS = 10 * 60 * 1e3;
54
306
  var DEFAULT_OPERATION_TIMEOUT_MS = 6e4;
55
307
  var DEFAULT_KEEP_ALIVE_MS = 5 * 60 * 1e3;
56
- var EXPECTED_RUNTIME_VERSION = workspaceRuntimeVersion();
57
- var RUNTIME_PKG = `@claxedo/workspace-runtime@${EXPECTED_RUNTIME_VERSION}`;
308
+ function runtimePackage(version) {
309
+ return `@claxedo/workspace-runtime@${version}`;
310
+ }
58
311
  var SNAPSHOT_CACHE_TTL_MS = 60 * 60 * 1e3;
59
312
  var snapshotCache = /* @__PURE__ */ new Map();
60
313
  var snapshotCacheState = /* @__PURE__ */ new WeakMap();
314
+ function isVercelSandboxFactory(value) {
315
+ const row = asRecord(value);
316
+ return typeof row?.create === "function" && typeof row.get === "function";
317
+ }
61
318
  function nameFor(workspaceId) {
62
319
  return `claxedo-${workspaceId}`;
63
320
  }
321
+ var TRANSIENT_MARKERS = ["timeout", "unavailable", "deadline", "pending"];
64
322
  function transientDriverError(err) {
65
- const shaped = err;
66
- const status = shaped.response?.status ?? shaped.status;
67
- if (typeof status === "number" && status >= 500) return true;
68
- const text = `${shaped.code ?? ""} ${shaped.name ?? ""} ${shaped.message ?? ""}`.toLowerCase();
69
- return text.includes("timeout") || text.includes("unavailable") || text.includes("deadline") || text.includes("pending");
323
+ return isTransientDriverError(err, TRANSIENT_MARKERS);
70
324
  }
71
325
  function network(input) {
72
326
  if (!input.net || input.net.mode === "allow-all") return void 0;
@@ -74,23 +328,53 @@ function network(input) {
74
328
  if (hosts.length === 0) return "deny-all";
75
329
  return { allow: hosts };
76
330
  }
77
- function vercelBrokeredNetworkPolicy(secrets) {
331
+ function vercelBrokeredNetworkPolicy(secrets, base) {
78
332
  const allow = {};
333
+ for (const [host, rules] of baseAllowEntries(base)) allow[host] = [...rules];
79
334
  for (const secret of secrets) {
80
335
  if (!secret.header) {
81
336
  throw new Error(
82
337
  `vercel brokered secret "${secret.name}" requires a header \u2014 the firewall injects the value as an HTTP header on egress`
83
338
  );
84
339
  }
340
+ if (secret.hosts.length === 0) {
341
+ throw new Error(`vercel brokered secret "${secret.name}" requires at least one host in its egress allowlist`);
342
+ }
343
+ const value = secret.scheme ? `${secret.scheme} ${secret.value}` : secret.value;
344
+ const methods = [...secret.methods ?? []];
345
+ const pathPrefixes = [...secret.pathPrefixes ?? []];
85
346
  for (const host of secret.hosts) {
86
- ;
87
- (allow[host] ??= []).push({ transform: [{ headers: { [secret.header]: secret.value } }] });
347
+ const rules = allow[host] ??= [];
348
+ for (const prefix of methods.length ? pathPrefixes : []) {
349
+ rules.push({
350
+ match: { path: { startsWith: prefix }, method: methods },
351
+ transform: [{ headers: { [secret.header]: value } }]
352
+ });
353
+ }
88
354
  }
89
355
  }
90
- return { allow };
356
+ const subnets = typeof base === "object" && base?.subnets ? { subnets: base.subnets } : {};
357
+ return { allow, ...subnets };
358
+ }
359
+ function baseAllowEntries(base) {
360
+ if (base === void 0 || base === "allow-all") return [["*", []]];
361
+ if (base === "deny-all") return [];
362
+ if (Array.isArray(base.allow)) return base.allow.map((host) => [host, []]);
363
+ return Object.entries(base.allow ?? {});
91
364
  }
92
365
  function createVercelSandboxDriver(options) {
93
- const sandboxFactory = options.sandbox ?? Sandbox;
366
+ let defaultFactory;
367
+ function resolveFactory() {
368
+ if (options.sandbox) return options.sandbox;
369
+ defaultFactory ??= import("@vercel/sandbox").then((sdk) => {
370
+ const factory = sdk.Sandbox;
371
+ if (!isVercelSandboxFactory(factory)) {
372
+ throw new Error("@vercel/sandbox does not expose the factory surface this driver uses");
373
+ }
374
+ return factory;
375
+ });
376
+ return defaultFactory;
377
+ }
94
378
  const runtime = options.runtime ?? DEFAULT_RUNTIME;
95
379
  const runtimePort = options.runtimePort ?? DEFAULT_WORKSPACE_RUNTIME_PORT;
96
380
  const runtimeCommand = options.runtimeCommand ?? DEFAULT_RUNTIME_COMMAND;
@@ -117,22 +401,17 @@ function createVercelSandboxDriver(options) {
117
401
  };
118
402
  }
119
403
  function bootEnv(input, hostId) {
120
- const env = {
121
- ...workspaceRuntimeTargetEnv({
122
- workspaceId: input.workspaceId,
123
- hostId,
124
- directory: workspaceDirectory(input),
125
- port: runtimePort,
126
- host: "0.0.0.0"
127
- }),
128
- ...workspaceRuntimeSourceEnv({ source: input.source }),
129
- ...input.env
130
- };
131
- if (options.runner) env.WORKSPACE_RUNTIME_RUNNER = options.runner;
132
- if (options.controlEnv?.relayJwksUrl) env.WORKSPACE_RUNTIME_RELAY_JWKS_URL = options.controlEnv.relayJwksUrl;
133
- if (options.controlEnv?.relayVerifyPem) env.WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM = options.controlEnv.relayVerifyPem;
134
- if (options.controlEnv?.managementJwksUrl) env.WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL = options.controlEnv.managementJwksUrl;
135
- return env;
404
+ return workspaceRuntimeBootEnv({
405
+ workspaceId: input.workspaceId,
406
+ hostId,
407
+ directory: workspaceDirectory(input),
408
+ port: runtimePort,
409
+ host: "0.0.0.0",
410
+ source: input.source,
411
+ env: input.env,
412
+ runner: options.runner,
413
+ controlEnv: options.controlEnv
414
+ });
136
415
  }
137
416
  async function runSetup(sandbox, command) {
138
417
  await sandbox.runCommand("bash", ["-c", command], {
@@ -141,7 +420,8 @@ function createVercelSandboxDriver(options) {
141
420
  }
142
421
  async function ensureSnapshot() {
143
422
  if (options.baseSnapshotId) return options.baseSnapshotId;
144
- const cacheKey = `${options.teamId}:${options.projectId}:${runtime}:${EXPECTED_RUNTIME_VERSION}`;
423
+ const expectedRuntimeVersion = workspaceRuntimeVersion();
424
+ const cacheKey = `${options.teamId}:${options.projectId}:${runtime}:${expectedRuntimeVersion}`;
145
425
  const hit = cache.get(cacheKey);
146
426
  const expiresAt = cacheState.expires.get(cacheKey);
147
427
  if (hit && expiresAt === void 0) {
@@ -152,7 +432,7 @@ function createVercelSandboxDriver(options) {
152
432
  const running = cacheState.builds.get(cacheKey);
153
433
  if (running) return running;
154
434
  const build = (async () => {
155
- const builder = await sandboxFactory.create({
435
+ const builder = await (await resolveFactory()).create({
156
436
  ...credentials(),
157
437
  runtime,
158
438
  ports: [runtimePort],
@@ -183,17 +463,11 @@ function createVercelSandboxDriver(options) {
183
463
  `sudo chown $(whoami) ${shell(RUNTIME_DIR)}`,
184
464
  `cd ${shell(RUNTIME_DIR)}`,
185
465
  "npm init -y",
186
- `npm install ${RUNTIME_PKG} --min-release-age=2`,
187
- `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!=='${EXPECTED_RUNTIME_VERSION}'){throw new Error('workspace-runtime snapshot package version mismatch: expected ${EXPECTED_RUNTIME_VERSION}, got '+actual)}"`,
466
+ `npm install ${runtimePackage(expectedRuntimeVersion)} --min-release-age=2`,
467
+ `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)}"`,
188
468
  "test -x node_modules/.bin/workspace-runtime",
189
- "test -x node_modules/.bin/claude-agent-acp",
190
- "test -x node_modules/.bin/codex-acp",
191
469
  `sudo ln -sf ${RUNTIME_DIR}/node_modules/.bin/workspace-runtime /usr/local/bin/workspace-runtime`,
192
- `sudo ln -sf ${RUNTIME_DIR}/node_modules/.bin/claude-agent-acp /usr/local/bin/claude-agent-acp`,
193
- `sudo ln -sf ${RUNTIME_DIR}/node_modules/.bin/codex-acp /usr/local/bin/codex-acp`,
194
- "command -v workspace-runtime",
195
- "command -v claude-agent-acp",
196
- "command -v codex-acp"
470
+ "command -v workspace-runtime"
197
471
  ].join(" && "));
198
472
  const snapshot = await builder.snapshot({
199
473
  expiration: 0,
@@ -247,22 +521,26 @@ function createVercelSandboxDriver(options) {
247
521
  const hostId = input.hostId ?? nameFor(input.workspaceId);
248
522
  const env = {
249
523
  ...bootEnv(input, hostId),
250
- ...await options.env?.(input, { id: hostId })
524
+ ...await options.env?.(input, { id: hostId }),
525
+ // Last, so no caller-supplied variable of the same name can stand in for
526
+ // a placeholder the firewall transform overwrites.
527
+ ...brokeredPlaceholderEnv(input.secrets)
251
528
  };
252
- const sandbox = await sandboxFactory.create({
529
+ const createPolicy = network(input);
530
+ const sandbox = await (await resolveFactory()).create({
253
531
  ...credentials(),
254
532
  source: await source(input),
255
533
  ports: [runtimePort],
256
534
  timeout: timeoutMs,
257
535
  env,
258
- ...network(input) ? { networkPolicy: network(input) } : {}
536
+ ...createPolicy ? { networkPolicy: createPolicy } : {}
259
537
  }).catch((err) => {
260
538
  if (transientDriverError(err)) return void 0;
261
539
  throw err;
262
540
  });
263
541
  if (!sandbox) return { provisioning: true, retryAfterMs: 2e3 };
264
542
  if (input.secrets?.length) {
265
- await sandbox.updateNetworkPolicy(vercelBrokeredNetworkPolicy(input.secrets), {
543
+ await sandbox.updateNetworkPolicy(vercelBrokeredNetworkPolicy(input.secrets, createPolicy), {
266
544
  signal: AbortSignal.timeout(operationTimeoutMs)
267
545
  });
268
546
  }
@@ -270,7 +548,7 @@ function createVercelSandboxDriver(options) {
270
548
  return readyTarget(input, sandbox, hostId);
271
549
  }
272
550
  async function sandboxById(target) {
273
- return sandboxFactory.get({
551
+ return (await resolveFactory()).get({
274
552
  ...credentials(),
275
553
  sandboxId: target.sandboxId
276
554
  });
@@ -282,7 +560,9 @@ function createVercelSandboxDriver(options) {
282
560
  hostStopBehavior: "terminates-host",
283
561
  hostResumeBehavior: "replacement-host",
284
562
  targetAccess: "relay",
285
- secretBrokering: "native"
563
+ secretBrokering: "native",
564
+ egressControl: "hosts",
565
+ persistence: sandboxDriverCatalog.vercel.metadata.persistence
286
566
  },
287
567
  ensureHost,
288
568
  resumeHost: (input) => ensureHost(input.ensure),
@@ -0,0 +1,87 @@
1
+ /**
2
+ * The egress allowlist a HOSTED, multi-tenant sandbox is provisioned with.
3
+ *
4
+ * A hosted sandbox runs agent-authored code on the operator's infrastructure,
5
+ * next to other tenants' sandboxes, holding a checkout of the user's private
6
+ * repository and (via `SandboxBrokeredSecret`) the ability to spend the
7
+ * operator's model credits. The default for that shape of workload is
8
+ * restrictive, not allow-all: the interesting attack is not "the agent
9
+ * downloads a package", it is "the agent POSTs the repository to a host the
10
+ * attacker controls". An allowlist is what makes that fail.
11
+ *
12
+ * Every entry below has to earn its place, so the list is assembled from three
13
+ * sources of truth rather than from taste:
14
+ *
15
+ * 1. DERIVED PER REQUEST — the control plane the sandbox reports back to and
16
+ * the git host it clones from. These differ per deployment and per
17
+ * workspace, so they are inputs, not constants.
18
+ * 2. MODEL PROVIDERS — the endpoints an agent harness must reach to do any
19
+ * work at all. Taken from the harness→provider mapping the credential
20
+ * layer already maintains (`PROVIDER_TO_GROUP` /`DEFAULT_ALLOWLIST` in
21
+ * claxedo-server's `network/types.ts`), narrowed to the API hostnames.
22
+ * 3. PACKAGE REGISTRIES — an agent that cannot install a dependency cannot
23
+ * edit a real repository. Same source, again narrowed.
24
+ *
25
+ * What is deliberately NOT here, despite being in that same default list:
26
+ * general-purpose object storage and CDN wildcards (`*.workers.dev`,
27
+ * `r2.cloudflarestorage.com`, `storage.googleapis.com`,
28
+ * `*.blob.core.windows.net`, `*.amazonaws.com`). Every one of those is a
29
+ * bucket an attacker can create in their own account, so allowlisting them
30
+ * hands back exactly the exfiltration channel the policy exists to close. A
31
+ * deployment that genuinely needs one adds it through `extraHosts` as an
32
+ * explicit, auditable decision.
33
+ *
34
+ * This module is data + string handling only, so it runs unchanged inside the
35
+ * hosted Worker (no DNS, no node builtins). That matters: name-based policy is
36
+ * the only kind the Worker can compute, which is why `egressControl` treats
37
+ * name filtering as the capability that qualifies a driver for hosted use.
38
+ */
39
+ import type { SandboxNetworkPolicy, SandboxSource } from ".";
40
+ /**
41
+ * Model-provider endpoints. Derived from the harness→provider groups in
42
+ * claxedo-server `network/types.ts` (`PROVIDER_TO_GROUP`): claude-sdk →
43
+ * anthropic, codex-app-server/cursor-sdk → openai,
44
+ * google → google_ai; plus the aggregator and catalog endpoints the engine
45
+ * itself fetches at startup.
46
+ */
47
+ export declare const SANDBOX_MODEL_PROVIDER_HOSTS: readonly ["api.anthropic.com", "api.openai.com", "generativelanguage.googleapis.com", "openrouter.ai", "models.dev", "opencode.ai"];
48
+ /**
49
+ * Package registries. Narrowed from the npm / pypi / rust / golang groups of
50
+ * `DEFAULT_ALLOWLIST` to the hostnames a package install actually contacts —
51
+ * the wildcard CDN entries in those groups are excluded on purpose (see the
52
+ * module comment).
53
+ */
54
+ export declare const SANDBOX_PACKAGE_REGISTRY_HOSTS: readonly ["registry.npmjs.org", "nodejs.org", "pypi.org", "files.pythonhosted.org", "crates.io", "static.crates.io", "index.crates.io", "proxy.golang.org", "sum.golang.org"];
55
+ /** The deployment-independent floor: providers + registries. */
56
+ export declare const SANDBOX_BASELINE_EGRESS_HOSTS: readonly ["api.anthropic.com", "api.openai.com", "generativelanguage.googleapis.com", "openrouter.ai", "models.dev", "opencode.ai", "registry.npmjs.org", "nodejs.org", "pypi.org", "files.pythonhosted.org", "crates.io", "static.crates.io", "index.crates.io", "proxy.golang.org", "sum.golang.org"];
57
+ /**
58
+ * The git host this workspace clones from.
59
+ *
60
+ * Scoped to the ONE repository host in the request rather than to a forge
61
+ * allowlist, matching how `authenticatedGitHubCloneSource` scopes the clone
62
+ * credential it brokers (`hosts: ["github.com"]`) — the credential and the
63
+ * network policy should describe the same reachable surface.
64
+ */
65
+ export declare function sandboxSourceHost(source: SandboxSource | undefined): string | undefined;
66
+ export type HostedSandboxNetworkPolicyInput = {
67
+ /**
68
+ * Control-plane and relay endpoints the runtime reports back to: the relay
69
+ * it tunnels through and fetches `WORKSPACE_RUNTIME_RELAY_JWKS_URL` from,
70
+ * and the control-plane origin serving `WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL`
71
+ * plus register/heartbeat. URLs or bare hostnames; blanks are ignored.
72
+ */
73
+ controlPlane: Array<string | undefined>;
74
+ /** Workspace source — its git host is allowed, nothing else on that forge. */
75
+ source?: SandboxSource;
76
+ /** Operator escape hatch for deployment-specific hosts. */
77
+ extraHosts?: readonly string[];
78
+ };
79
+ /**
80
+ * Build the restricted policy for a hosted sandbox.
81
+ *
82
+ * Always `mode: "restricted"`. There is no input that makes this return
83
+ * allow-all — a hosted sandbox with an empty or unresolvable control-plane URL
84
+ * is a misconfiguration, and answering it with "then allow everything" is how
85
+ * the gap this closes came to exist in the first place.
86
+ */
87
+ export declare function hostedSandboxNetworkPolicy(input: HostedSandboxNetworkPolicyInput): SandboxNetworkPolicy;
@@ -0,0 +1,61 @@
1
+ // src/hosted-network-policy.ts
2
+ var SANDBOX_MODEL_PROVIDER_HOSTS = [
3
+ "api.anthropic.com",
4
+ "api.openai.com",
5
+ "generativelanguage.googleapis.com",
6
+ "openrouter.ai",
7
+ // Model catalog + engine config, fetched on runner startup.
8
+ "models.dev",
9
+ "opencode.ai"
10
+ ];
11
+ var SANDBOX_PACKAGE_REGISTRY_HOSTS = [
12
+ "registry.npmjs.org",
13
+ "nodejs.org",
14
+ "pypi.org",
15
+ "files.pythonhosted.org",
16
+ "crates.io",
17
+ "static.crates.io",
18
+ "index.crates.io",
19
+ "proxy.golang.org",
20
+ "sum.golang.org"
21
+ ];
22
+ var SANDBOX_BASELINE_EGRESS_HOSTS = [
23
+ ...SANDBOX_MODEL_PROVIDER_HOSTS,
24
+ ...SANDBOX_PACKAGE_REGISTRY_HOSTS
25
+ ];
26
+ function hostOf(value) {
27
+ const raw = value?.trim();
28
+ if (!raw) return void 0;
29
+ try {
30
+ const host = new URL(raw).hostname.trim().toLowerCase();
31
+ return host ? host : void 0;
32
+ } catch {
33
+ return void 0;
34
+ }
35
+ }
36
+ function sandboxSourceHost(source) {
37
+ if (!source || source.kind !== "git") return void 0;
38
+ return hostOf(source.repoUrl);
39
+ }
40
+ function hostedSandboxNetworkPolicy(input) {
41
+ const hosts = /* @__PURE__ */ new Set();
42
+ for (const entry of input.controlPlane) {
43
+ const host = hostOf(entry) ?? entry?.trim().toLowerCase();
44
+ if (host) hosts.add(host);
45
+ }
46
+ const sourceHost = sandboxSourceHost(input.source);
47
+ if (sourceHost) hosts.add(sourceHost);
48
+ for (const host of SANDBOX_BASELINE_EGRESS_HOSTS) hosts.add(host);
49
+ for (const host of input.extraHosts ?? []) {
50
+ const trimmed = host.trim().toLowerCase();
51
+ if (trimmed) hosts.add(trimmed);
52
+ }
53
+ return { mode: "restricted", hosts: [...hosts] };
54
+ }
55
+ export {
56
+ SANDBOX_BASELINE_EGRESS_HOSTS,
57
+ SANDBOX_MODEL_PROVIDER_HOSTS,
58
+ SANDBOX_PACKAGE_REGISTRY_HOSTS,
59
+ hostedSandboxNetworkPolicy,
60
+ sandboxSourceHost
61
+ };