withruntime 0.8.4 → 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 (132) hide show
  1. package/BLAXEL.md +4 -4
  2. package/COMPATIBILITY.md +145 -0
  3. package/DAYTONA.md +13 -7
  4. package/E2B.md +34 -20
  5. package/README.md +22 -11
  6. package/README.src.md +22 -11
  7. package/VERCEL.md +13 -3
  8. package/compatibility-lock.json +357 -0
  9. package/dist/ai-harness/index.js +2 -2
  10. package/dist/api-defaults.d.ts +17 -0
  11. package/dist/api-defaults.js +22 -0
  12. package/dist/blaxel/errors.js +1 -1
  13. package/dist/blaxel/lookup.d.ts +3 -2
  14. package/dist/blaxel/lookup.js +3 -2
  15. package/dist/blaxel/pagination.d.ts +2 -2
  16. package/dist/blaxel/pagination.js +45 -17
  17. package/dist/blaxel/preview.d.ts +3 -2
  18. package/dist/blaxel/preview.js +16 -14
  19. package/dist/blaxel/process.d.ts +1 -1
  20. package/dist/blaxel/process.js +66 -11
  21. package/dist/blaxel/sandbox.d.ts +2 -0
  22. package/dist/blaxel/sandbox.js +2 -0
  23. package/dist/blaxel/snapshot.js +3 -5
  24. package/dist/cli-words.d.ts +27 -0
  25. package/dist/cli-words.js +135 -0
  26. package/dist/cli.d.ts +2 -0
  27. package/dist/cli.js +351 -85
  28. package/dist/client.d.ts +33 -4
  29. package/dist/client.js +47 -12
  30. package/dist/cloudflare/identity.d.ts +7 -0
  31. package/dist/cloudflare/identity.js +13 -0
  32. package/dist/cloudflare/index.d.ts +303 -0
  33. package/dist/cloudflare/index.js +592 -0
  34. package/dist/cloudflare/worker.d.ts +419 -0
  35. package/dist/cloudflare/worker.js +447 -0
  36. package/dist/codesandbox/guest-service.d.ts +31 -0
  37. package/dist/codesandbox/guest-service.js +490 -0
  38. package/dist/codesandbox/index.d.ts +386 -0
  39. package/dist/codesandbox/index.js +1347 -0
  40. package/dist/compat/core.d.ts +40 -0
  41. package/dist/compat/core.js +238 -0
  42. package/dist/compat/files.d.ts +26 -0
  43. package/dist/compat/files.js +133 -0
  44. package/dist/compat/shell.d.ts +4 -0
  45. package/dist/compat/shell.js +165 -0
  46. package/dist/credentials.d.ts +6 -1
  47. package/dist/credentials.js +107 -38
  48. package/dist/daytona/context.js +2 -1
  49. package/dist/daytona/daytona.js +11 -4
  50. package/dist/daytona/filesystem.d.ts +17 -4
  51. package/dist/daytona/filesystem.js +121 -38
  52. package/dist/daytona/index.d.ts +1 -1
  53. package/dist/daytona/process.d.ts +1 -1
  54. package/dist/daytona/process.js +27 -4
  55. package/dist/daytona/sandbox.d.ts +1 -1
  56. package/dist/daytona/sandbox.js +16 -7
  57. package/dist/e2b/client.js +1 -1
  58. package/dist/e2b/code-interpreter.d.ts +4 -5
  59. package/dist/e2b/code-interpreter.js +54 -56
  60. package/dist/e2b/commands.d.ts +35 -12
  61. package/dist/e2b/commands.js +169 -73
  62. package/dist/e2b/errors.js +1 -1
  63. package/dist/e2b/filesystem.d.ts +8 -6
  64. package/dist/e2b/filesystem.js +127 -28
  65. package/dist/e2b/index.d.ts +1 -0
  66. package/dist/e2b/index.js +1 -0
  67. package/dist/e2b/pty.d.ts +24 -0
  68. package/dist/e2b/pty.js +80 -0
  69. package/dist/e2b/sandbox.d.ts +6 -4
  70. package/dist/e2b/sandbox.js +59 -25
  71. package/dist/errors.d.ts +8 -0
  72. package/dist/errors.js +15 -2
  73. package/dist/file-source.d.ts +16 -0
  74. package/dist/file-source.js +162 -0
  75. package/dist/freestyle/index.d.ts +309 -0
  76. package/dist/freestyle/index.js +343 -0
  77. package/dist/index.d.ts +3 -1
  78. package/dist/login.js +16 -10
  79. package/dist/mcp.js +2 -2
  80. package/dist/modal/index.d.ts +205 -0
  81. package/dist/modal/index.js +486 -0
  82. package/dist/network-cli.js +112 -44
  83. package/dist/openai-agents/index.js +18 -71
  84. package/dist/page.d.ts +1 -1
  85. package/dist/page.js +2 -2
  86. package/dist/products/audit.d.ts +3 -0
  87. package/dist/products/index.d.ts +14 -0
  88. package/dist/products/index.js +12 -0
  89. package/dist/products/interpreter.d.ts +11 -4
  90. package/dist/products/interpreter.js +33 -9
  91. package/dist/products/jobs.js +3 -2
  92. package/dist/products/limits.d.ts +10 -0
  93. package/dist/products/observability.d.ts +44 -0
  94. package/dist/products/observability.js +6 -0
  95. package/dist/products/previews.d.ts +18 -4
  96. package/dist/products/previews.js +14 -5
  97. package/dist/products/tailscale.d.ts +57 -0
  98. package/dist/products/tailscale.js +17 -0
  99. package/dist/products/volumes.d.ts +42 -0
  100. package/dist/products/volumes.js +36 -0
  101. package/dist/products/watch.d.ts +25 -4
  102. package/dist/products/watch.js +132 -18
  103. package/dist/proxy.js +3 -1
  104. package/dist/runloop/index.d.ts +326 -0
  105. package/dist/runloop/index.js +535 -0
  106. package/dist/runloop/pagination.d.ts +46 -0
  107. package/dist/runloop/pagination.js +82 -0
  108. package/dist/runloop/snapshots.d.ts +52 -0
  109. package/dist/runloop/snapshots.js +244 -0
  110. package/dist/sandbox.d.ts +76 -16
  111. package/dist/sandbox.js +696 -135
  112. package/dist/snapshots.d.ts +20 -5
  113. package/dist/snapshots.js +11 -1
  114. package/dist/sprites/index.d.ts +251 -0
  115. package/dist/sprites/index.js +588 -0
  116. package/dist/tar.d.ts +10 -1
  117. package/dist/tar.js +311 -35
  118. package/dist/tools/index.js +2 -1
  119. package/dist/transport.d.ts +23 -8
  120. package/dist/transport.js +247 -127
  121. package/dist/types.d.ts +52 -3
  122. package/dist/vercel/command.d.ts +1 -1
  123. package/dist/vercel/command.js +15 -3
  124. package/dist/vercel/errors.d.ts +2 -2
  125. package/dist/vercel/errors.js +19 -2
  126. package/dist/vercel/filesystem.d.ts +21 -18
  127. package/dist/vercel/filesystem.js +95 -48
  128. package/dist/vercel/sandbox.d.ts +8 -8
  129. package/dist/vercel/sandbox.js +159 -64
  130. package/dist/vercel/snapshot.d.ts +3 -2
  131. package/dist/vercel/snapshot.js +9 -5
  132. package/package.json +46 -3
@@ -36,6 +36,10 @@ function leaseSeconds(timeoutMs) {
36
36
  return Math.max(seconds, MIN_LEASE_SECONDS);
37
37
  }
38
38
  function refuseCreate(params) {
39
+ if (params.keepLastSnapshots !== undefined)
40
+ throw new NotSupportedError("Snapshot count and eviction policies (keepLastSnapshots)", "Omit it; manage saved snapshots explicitly with Snapshot.list and snapshot.delete.");
41
+ if (params.ports !== undefined)
42
+ validatePorts(params.ports);
39
43
  if (params.mounts && Object.keys(params.mounts).length)
40
44
  throw new NotSupportedError("Vercel Drives (mounts)", "Use a Runtime volume: withruntime: { create: { volumes: [{ volumeId, path }] } } (runtime.volumes creates one).");
41
45
  if (params.networkId !== undefined)
@@ -45,6 +49,11 @@ function refuseCreate(params) {
45
49
  if (params.failoverRegions?.length)
46
50
  throw new NotSupportedError("Failover regions (failoverRegions)", "Remove it: Runtime runs in one US region.");
47
51
  }
52
+ function validatePorts(ports) {
53
+ if (!Array.isArray(ports) ||
54
+ ports.some((port) => !Number.isSafeInteger(port) || port < 1 || port > 65_535))
55
+ throw new RangeError("ports must contain only integers from 1 to 65535.");
56
+ }
48
57
  /** Vercel's network policy as Runtime's rules. Header injection and
49
58
  * forwarding rules have no Runtime counterpart and are refused. */
50
59
  export function networkRules(policy) {
@@ -111,15 +120,15 @@ function statusOf(state) {
111
120
  /** Runtime's `/workspace` stands for Vercel's `/vercel/sandbox`: relative
112
121
  * paths resolve against it, and paths under /vercel/sandbox are rewritten. */
113
122
  export function toRuntimePath(path, cwd) {
114
- const base = cwd === undefined ? HOME : toVercelPath(cwd);
115
- const absolute = path.startsWith("/") ? path : `${base}/${path.replace(/^\.\/?/, "")}`;
123
+ const base = cwd === undefined ? HOME : toVercelPath(cwd).replace(/\/+$/, "");
124
+ const absolute = path.startsWith("/") ? path : `${base}/${path.replace(/^(?:\.(?:\/+|$))+/, "")}`;
116
125
  const normal = absolute.replace(/\/+$/, "") || "/";
117
126
  if (normal === HOME || normal.startsWith(`${HOME}/`))
118
127
  return RUNTIME_HOME + normal.slice(HOME.length);
119
128
  return normal;
120
129
  }
121
130
  function toVercelPath(path) {
122
- return path.startsWith("/") ? path : `${HOME}/${path.replace(/^\.\/?/, "")}`;
131
+ return path.startsWith("/") ? path : `${HOME}/${path.replace(/^(?:\.(?:\/+|$))+/, "")}`;
123
132
  }
124
133
  /** A sandbox with Vercel's methods. `sandbox.withruntime` is the Runtime
125
134
  * sandbox underneath, for anything Vercel has no name for. */
@@ -143,9 +152,9 @@ export class Sandbox {
143
152
  this.fs = new FileSystem({
144
153
  name: this.name,
145
154
  files: async () => (await this.#live()).files,
146
- run: async (argv) => {
155
+ run: async (argv, options) => {
147
156
  const live = await this.#live();
148
- return live.exec(argv);
157
+ return live.exec(argv, options);
149
158
  },
150
159
  resolve: (path, cwd) => toRuntimePath(path, cwd),
151
160
  });
@@ -227,6 +236,8 @@ export class Sandbox {
227
236
  refuseCreate(params);
228
237
  const client = clientFor(params);
229
238
  const persistent = params.persistent ?? true;
239
+ if (params.snapshotExpiration !== undefined && persistent)
240
+ retentionDays(params.snapshotExpiration);
230
241
  const vcpus = params.resources?.vcpus ?? DEFAULT_VCPUS;
231
242
  const source = await guard(() => resolveImage(client, params));
232
243
  const input = {
@@ -300,15 +311,16 @@ export class Sandbox {
300
311
  /** A sandbox by name (or Runtime id), woken if it is paused unless
301
312
  * `resume: false`. */
302
313
  static async get(params) {
314
+ params.signal?.throwIfAborted();
303
315
  const client = clientFor(params);
304
- const runtime = await guard(() => find(client, params.name), params.name);
316
+ const runtime = await guard(() => find(client, params.name, params), params.name);
305
317
  const sandbox = new Sandbox({
306
318
  runtime,
307
319
  client,
308
320
  params: params.onResume ? { onResume: params.onResume } : {},
309
321
  });
310
322
  if (params.resume !== false)
311
- await sandbox.#live();
323
+ await sandbox.#live(params);
312
324
  return sandbox;
313
325
  }
314
326
  /** The named sandbox, or a new one with that name when there is none. */
@@ -329,9 +341,22 @@ export class Sandbox {
329
341
  /** A new sandbox copied from a running one, memory and all (a Runtime
330
342
  * fork). `sourceSandbox` is its name. */
331
343
  static async fork(params) {
332
- const unsupported = ["resources", "ports", "timeout", "networkPolicy", "image", "tags", "env"].find((field) => params[field] !== undefined);
344
+ const unsupported = [
345
+ "resources",
346
+ "ports",
347
+ "timeout",
348
+ "networkPolicy",
349
+ "image",
350
+ "tags",
351
+ "env",
352
+ "persistent",
353
+ "snapshotExpiration",
354
+ "keepLastSnapshots",
355
+ "onResume",
356
+ "runtime",
357
+ ].find((field) => params[field] !== undefined);
333
358
  if (unsupported)
334
- throw new NotSupportedError(`Overriding ${unsupported} on a fork`, "Fork without it; the copy keeps the source's machine, lease and rules.");
359
+ throw new NotSupportedError(`Overriding ${unsupported} on a fork`, "Omit the override to use Runtime's existing fork behavior.");
335
360
  refuseCreate(params);
336
361
  const source = await Sandbox.get({ ...params, name: params.sourceSandbox });
337
362
  const copy = await guard(() => source.#rt.fork({ ...(params.name ? { name: params.name } : {}), ...signalOf(params) }), params.sourceSandbox);
@@ -344,6 +369,7 @@ export class Sandbox {
344
369
  /** Running and stopped (paused) sandboxes, oldest first, with Vercel's
345
370
  * paginator: `for await (const s of await Sandbox.list())`. */
346
371
  static async list(params = {}) {
372
+ params.signal?.throwIfAborted();
347
373
  for (const field of ["since", "until", "cursor", "sortBy", "sortOrder", "namePrefix"])
348
374
  if (params[field] !== undefined)
349
375
  throw new NotSupportedError(`Listing sandboxes by ${field}`, "List them all (oldest first) and filter the result yourself.");
@@ -351,13 +377,14 @@ export class Sandbox {
351
377
  const page = await guard(() => client.sandboxes.list({
352
378
  ...(params.tags ? { labels: params.tags } : {}),
353
379
  ...(params.limit ? { limit: Math.min(params.limit, 100) } : {}),
354
- }));
355
- return paginate(page);
380
+ }, signalOf(params)));
381
+ return paginate(page, params.signal);
356
382
  }
357
383
  // ---- commands ---------------------------------------------------------
358
384
  /** Wakes the sandbox if it was paused (a stopped persistent sandbox, in
359
385
  * Vercel's words), as Vercel resumes one on the next call. */
360
- async #live() {
386
+ async #live(opts = {}) {
387
+ opts.signal?.throwIfAborted();
361
388
  if (this.#deleted)
362
389
  throw new APIError(new Response(null, { status: 410 }), {
363
390
  message: `Sandbox ${this.name} was deleted.`,
@@ -365,35 +392,43 @@ export class Sandbox {
365
392
  });
366
393
  const state = this.#rt.state;
367
394
  if (state === "paused" || state === "pausing")
368
- await this.#wake();
395
+ await this.#wake(opts);
369
396
  return this.#rt;
370
397
  }
371
- async #wake() {
372
- await guard(() => this.#rt.wake({}), this.name);
398
+ async #wake(opts = {}) {
399
+ await guard(() => this.#rt.wake(signalOf(opts)), this.name);
373
400
  await this.#onResume?.(this);
374
401
  }
375
402
  /** Runs `work` on the live sandbox; if the lease paused it meanwhile, wakes
376
403
  * it and runs `work` once more. */
377
- async #withResume(work) {
378
- const runtime = await this.#live();
404
+ async #withResume(work, opts = {}) {
405
+ opts.signal?.throwIfAborted();
406
+ const runtime = await this.#live(opts);
379
407
  try {
380
408
  return await work(runtime);
381
409
  }
382
410
  catch (error) {
383
411
  if (!(error instanceof RuntimeError && error.code === "sandbox_paused"))
384
412
  throw translate(error, this.name);
385
- await guard(() => runtime.refresh(), this.name);
386
- await this.#wake();
413
+ await guard(() => runtime.refresh(signalOf(opts)), this.name);
414
+ await this.#wake(opts);
387
415
  return guard(() => work(this.#rt), this.name);
388
416
  }
389
417
  }
390
- async #linkHome(text) {
418
+ async #linkHome(text, opts = {}) {
419
+ opts.signal?.throwIfAborted();
391
420
  if (this.#linked || !text.includes(HOME))
392
421
  return this.#linked;
393
422
  this.#linked = (async () => {
394
- await this.#rt.exec(HOME_LINK).catch(() => undefined);
423
+ await this.#rt.exec(HOME_LINK, signalOf(opts)).catch(() => opts.signal?.throwIfAborted());
395
424
  })();
396
- return this.#linked;
425
+ try {
426
+ await this.#linked;
427
+ }
428
+ catch (error) {
429
+ this.#linked = undefined;
430
+ throw error;
431
+ }
397
432
  }
398
433
  async runCommand(commandOrParams, args, opts = {}) {
399
434
  const params = typeof commandOrParams === "string"
@@ -406,18 +441,19 @@ export class Sandbox {
406
441
  ];
407
442
  const cwd = params.cwd === undefined ? RUNTIME_HOME : toRuntimePath(params.cwd);
408
443
  const env = { ...this.#env, ...params.env };
409
- await this.#live();
410
- await this.#linkHome(`${argv.join(" ")}\n${params.cwd ?? ""}\n${Object.values(env).join("\n")}`);
444
+ await this.#live(params);
445
+ await this.#linkHome(`${argv.join(" ")}\n${params.cwd ?? ""}\n${Object.values(env).join("\n")}`, params);
411
446
  const timeoutMs = params.timeoutMs ?? LONGEST_MS;
412
447
  const common = {
413
448
  cwd,
414
449
  ...(Object.keys(env).length ? { env } : {}),
415
450
  timeoutMs,
451
+ ...signalOf(params),
416
452
  };
417
453
  const startedAt = Date.now();
418
454
  const vercelCwd = params.cwd === undefined ? HOME : toVercelPath(params.cwd);
419
455
  if (params.detached) {
420
- const process = await this.#withResume((runtime) => runtime.spawn(argv, common));
456
+ const process = await this.#withResume((runtime) => runtime.spawn(argv, common), params);
421
457
  const command = new Command({
422
458
  id: process.id,
423
459
  cwd: vercelCwd,
@@ -438,8 +474,7 @@ export class Sandbox {
438
474
  ...common,
439
475
  onStdout: record("stdout", params.stdout),
440
476
  onStderr: record("stderr", params.stderr),
441
- ...signalOf(params),
442
- }));
477
+ }), params);
443
478
  return new CommandFinished({
444
479
  id: result.processId ?? `cmd_${startedAt}`,
445
480
  cwd: vercelCwd,
@@ -449,8 +484,8 @@ export class Sandbox {
449
484
  }, exitCodeOf(result.exitCode, result.timedOut), result.durationMs ?? Date.now() - startedAt);
450
485
  }
451
486
  /** A command started earlier, by its cmdId. */
452
- async getCommand(cmdId, _opts = {}) {
453
- const process = await this.#withResume((runtime) => runtime.processes.get(cmdId));
487
+ async getCommand(cmdId, opts = {}) {
488
+ const process = await this.#withResume((runtime) => runtime.processes.get(cmdId, signalOf(opts)), opts);
454
489
  return new Command({
455
490
  id: process.id,
456
491
  cwd: process.info.cwd,
@@ -460,9 +495,9 @@ export class Sandbox {
460
495
  });
461
496
  }
462
497
  // ---- files ------------------------------------------------------------
463
- async mkDir(path, _opts = {}) {
498
+ async mkDir(path, opts = {}) {
464
499
  const target = toRuntimePath(path);
465
- await this.#withResume((runtime) => runtime.files.mkdir(target, { parents: true }));
500
+ await this.#withResume((runtime) => runtime.files.mkdir(target, { parents: true, ...signalOf(opts) }), opts);
466
501
  }
467
502
  /** The file's bytes as a Node stream, or null when there is no file. */
468
503
  async readFile(file, opts = {}) {
@@ -476,7 +511,7 @@ export class Sandbox {
476
511
  async readFileToBuffer(file, opts = {}) {
477
512
  const target = toRuntimePath(file.path, file.cwd);
478
513
  try {
479
- return Buffer.from(await this.#withResume((runtime) => runtime.files.read(target, signalOf(opts))));
514
+ return Buffer.from(await this.#withResume((runtime) => runtime.files.read(target, signalOf(opts)), opts));
480
515
  }
481
516
  catch (error) {
482
517
  if (error instanceof APIError && error.response.status === 404)
@@ -489,39 +524,42 @@ export class Sandbox {
489
524
  const buffer = await this.readFileToBuffer(src, opts);
490
525
  if (buffer === null)
491
526
  return null;
527
+ opts.signal?.throwIfAborted();
492
528
  const { mkdir, writeFile } = await import("node:fs/promises");
493
529
  const paths = await import("node:path");
494
530
  const target = paths.resolve(dst.cwd ?? process.cwd(), dst.path);
531
+ opts.signal?.throwIfAborted();
495
532
  if (opts.mkdirRecursive)
496
533
  await mkdir(paths.dirname(target), { recursive: true });
497
- await writeFile(target, buffer);
534
+ await writeFile(target, buffer, signalOf(opts));
498
535
  return target;
499
536
  }
500
537
  /** Writes files, making their directories. Relative paths land in the
501
538
  * working directory; `mode` sets permissions. */
502
- async writeFiles(files, _opts = {}) {
539
+ async writeFiles(files, opts = {}) {
503
540
  const modes = [];
504
541
  await this.#withResume(async (runtime) => {
505
542
  for (const file of files) {
506
543
  const target = toRuntimePath(file.path);
507
- await runtime.files.write(target, file.content);
544
+ await runtime.files.write(target, file.content, signalOf(opts));
508
545
  if (file.mode !== undefined)
509
546
  modes.push(file.mode.toString(8), target);
510
547
  }
511
548
  if (!modes.length)
512
549
  return;
513
550
  const script = 'while [ "$#" -gt 0 ]; do chmod "$1" "$2" || exit 1; shift 2; done';
514
- const result = await runtime.exec(["sh", "-c", script, "sh", ...modes]);
551
+ const result = await runtime.exec(["sh", "-c", script, "sh", ...modes], signalOf(opts));
515
552
  if (result.exitCode !== 0)
516
553
  throw new APIError(new Response(null, { status: 400 }), {
517
554
  message: `Setting file modes failed: ${result.stderr.trim()}`,
518
555
  sandboxName: this.name,
519
556
  });
520
- });
557
+ }, opts);
521
558
  }
522
559
  // ---- ports ------------------------------------------------------------
523
- async #share(port) {
524
- const preview = await guard(() => this.#rt.previews.create(port, { visibility: "public" }), this.name);
560
+ async #share(port, opts = {}) {
561
+ opts.signal?.throwIfAborted();
562
+ const preview = await guard(() => this.#rt.previews.create(port, { visibility: "public" }, signalOf(opts)), this.name);
525
563
  this.#routes.set(port, preview.url.replace(/\/$/, ""));
526
564
  }
527
565
  /** The public address of a port listed in `ports`, such as
@@ -566,53 +604,99 @@ export class Sandbox {
566
604
  async extendTimeout(duration, opts = {}) {
567
605
  if (!Number.isFinite(duration) || duration <= 0)
568
606
  throw new RangeError(`duration must be a positive number of milliseconds, not ${duration}.`);
569
- await this.#withResume((runtime) => runtime.extend(Math.ceil(duration / 1000), signalOf(opts)));
607
+ await this.#withResume((runtime) => runtime.extend(Math.ceil(duration / 1000), signalOf(opts)), opts);
570
608
  }
571
609
  /** Replaces the network rules; returns the policy given. */
572
- async updateNetworkPolicy(networkPolicy, _opts = {}) {
610
+ async updateNetworkPolicy(networkPolicy, opts = {}) {
611
+ opts.signal?.throwIfAborted();
573
612
  const rules = networkRules(networkPolicy);
574
- await this.#withResume((runtime) => runtime.network.set(rules));
613
+ await this.#withResume((runtime) => runtime.network.set(rules, signalOf(opts)), opts);
575
614
  return networkPolicy;
576
615
  }
577
616
  /** Changes what Runtime can change on a sandbox: `timeout`, `ports`,
578
617
  * `networkPolicy` and `snapshotExpiration`. Anything else is refused before
579
618
  * anything changes. */
580
- async update(params, _opts = {}) {
619
+ async update(params, opts = {}) {
620
+ opts.signal?.throwIfAborted();
581
621
  const allowed = new Set(["timeout", "ports", "networkPolicy", "snapshotExpiration"]);
582
622
  const other = Object.keys(params).find((key) => params[key] !== undefined && !allowed.has(key));
583
623
  if (other)
584
624
  throw new NotSupportedError(`Changing ${other} on an existing sandbox`, "Create a new sandbox with it (Runtime cannot change a sandbox's machine, persistence, tags or region after creation).");
625
+ const rules = params.networkPolicy === undefined ? undefined : networkRules(params.networkPolicy);
626
+ const retention = params.snapshotExpiration === undefined
627
+ ? undefined
628
+ : retentionDays(params.snapshotExpiration);
629
+ if (params.ports !== undefined)
630
+ validatePorts(params.ports);
631
+ const wanted = params.ports === undefined ? undefined : new Set(params.ports);
585
632
  if (params.timeout !== undefined) {
586
633
  const current = this.timeout;
587
634
  if (params.timeout < current)
588
635
  throw new NotSupportedError("Shortening a sandbox's timeout", "Runtime leases only move later. Call stop() when the work is done.");
589
636
  if (params.timeout > current)
590
- await this.extendTimeout(params.timeout - current);
637
+ await this.extendTimeout(params.timeout - current, opts);
591
638
  }
592
- if (params.networkPolicy !== undefined)
593
- await this.updateNetworkPolicy(params.networkPolicy);
594
- if (params.snapshotExpiration !== undefined)
595
- await guard(() => this.#rt.setRetention(retentionDays(params.snapshotExpiration)), this.name);
596
- if (params.ports !== undefined) {
597
- const wanted = new Set(params.ports);
639
+ if (rules !== undefined)
640
+ await this.#withResume((runtime) => runtime.network.set(rules, signalOf(opts)), opts);
641
+ if (retention !== undefined)
642
+ await guard(() => this.#rt.setRetention(retention, signalOf(opts)), this.name);
643
+ if (wanted !== undefined) {
598
644
  for (const port of [...this.#routes.keys()])
599
645
  if (!wanted.has(port)) {
600
- await guard(() => this.#rt.previews.delete(port), this.name);
646
+ await guard(() => this.#rt.previews.delete(port, signalOf(opts)), this.name);
601
647
  this.#routes.delete(port);
602
648
  }
603
649
  for (const port of wanted)
604
650
  if (!this.#routes.has(port))
605
- await this.#share(port);
651
+ await this.#share(port, opts);
606
652
  }
607
653
  }
608
- /** Keeps the sandbox's whole machine as a Runtime snapshot (files, memory
609
- * and running processes), then stops the sandbox, as Vercel does. Start
654
+ /** Keeps the sandbox's filesystem as a disk-only snapshot, then stops
655
+ * the source. Restoring it starts fresh processes, as Vercel does. Start
610
656
  * from it with `Sandbox.create({ source: { type: "snapshot", snapshotId } })`. */
611
657
  async snapshot(opts = {}) {
658
+ opts.signal?.throwIfAborted();
659
+ const retention = opts.expiration === undefined ? undefined : retentionDays(opts.expiration);
612
660
  const taken = await this.#withResume((runtime) => runtime.snapshot({
613
- ...(opts.expiration !== undefined ? { retentionDays: retentionDays(opts.expiration) } : {}),
614
- }));
615
- await guard(() => this.#rt.stop({ wait: false }), this.name);
661
+ mode: "disk",
662
+ ...(retention !== undefined ? { retentionDays: retention } : {}),
663
+ ...signalOf(opts),
664
+ }), opts);
665
+ try {
666
+ await guard(() => this.#rt.stop(signalOf(opts)), this.name);
667
+ if (this.#rt.state !== "stopped")
668
+ await guard(() => this.#rt.waitFor("stopped", signalOf(opts)), this.name);
669
+ }
670
+ catch (error) {
671
+ const recovery = { snapshotId: taken.id, sourceSandboxId: this.#rt.id };
672
+ if (error instanceof APIError) {
673
+ const json = error.json;
674
+ error.json = {
675
+ ...json,
676
+ error: {
677
+ ...json?.error,
678
+ details: { ...(json?.error?.details ?? {}), ...recovery },
679
+ },
680
+ };
681
+ }
682
+ else if (error instanceof Error && Object.isExtensible(error)) {
683
+ Object.assign(error, recovery);
684
+ }
685
+ throw error;
686
+ }
687
+ if (this.#rt.state !== "stopped") {
688
+ const code = "snapshot_source_stop_timeout";
689
+ const message = `Snapshot ${taken.id} was saved, but sandbox ${this.name} did not finish stopping.`;
690
+ const error = new APIError(new Response(null, { status: 409 }), {
691
+ message,
692
+ sandboxName: this.name,
693
+ json: {
694
+ error: { code, message, details: { snapshotId: taken.id, sourceSandboxId: this.#rt.id } },
695
+ },
696
+ });
697
+ error.code = code;
698
+ throw error;
699
+ }
616
700
  return new Snapshot(taken, this.#client);
617
701
  }
618
702
  // ---- what Runtime does differently -----------------------------------
@@ -656,6 +740,8 @@ function usersRefused() {
656
740
  /** Milliseconds as whole days for Runtime's retention: 1 to 365, and 0 (no
657
741
  * expiration) as 365. */
658
742
  export function retentionDays(ms) {
743
+ if (!Number.isFinite(ms) || ms < 0)
744
+ throw new RangeError(`Snapshot expiration must be a nonnegative finite number of milliseconds, not ${ms}.`);
659
745
  if (ms === 0)
660
746
  return 365;
661
747
  return Math.min(365, Math.max(1, Math.ceil(ms / 86_400_000)));
@@ -671,11 +757,13 @@ async function pipe(command, params) {
671
757
  }
672
758
  /** The newest live sandbox with this name, or the sandbox with this Runtime
673
759
  * id. A 404 when there is none. */
674
- async function find(client, name) {
760
+ async function find(client, name, opts = {}) {
761
+ opts.signal?.throwIfAborted();
675
762
  if (UUID.test(name))
676
- return client.sandboxes.get(name);
677
- const page = await client.sandboxes.list({ name });
763
+ return client.sandboxes.get(name, signalOf(opts));
764
+ const page = await client.sandboxes.list({ name }, signalOf(opts));
678
765
  const all = await page.toArray(1000);
766
+ opts.signal?.throwIfAborted();
679
767
  const found = all.filter((one) => one.state !== "stopped").at(-1);
680
768
  if (!found)
681
769
  throw new RuntimeError({
@@ -705,22 +793,29 @@ function itemOf(runtime) {
705
793
  }
706
794
  /** Vercel's paginator over a Runtime page: the first page's fields, async
707
795
  * iteration over every item, `pages()` and `toArray()`. */
708
- function paginate(first) {
796
+ function paginate(first, signal) {
709
797
  const toPage = (page) => ({
710
798
  sandboxes: page.data.map(itemOf),
711
799
  pagination: { count: page.data.length, next: page.nextCursor },
712
800
  });
713
801
  return Object.assign(toPage(first), {
714
802
  async *pages() {
715
- for await (const page of first.pages())
803
+ for await (const page of first.pages()) {
804
+ signal?.throwIfAborted();
716
805
  yield toPage(page);
806
+ }
717
807
  },
718
808
  async *[Symbol.asyncIterator]() {
719
- for await (const item of first)
809
+ for await (const item of first) {
810
+ signal?.throwIfAborted();
720
811
  yield itemOf(item);
812
+ }
721
813
  },
722
814
  async toArray() {
723
- return (await first.toArray()).map(itemOf);
815
+ signal?.throwIfAborted();
816
+ const items = await first.toArray();
817
+ signal?.throwIfAborted();
818
+ return items.map(itemOf);
724
819
  },
725
820
  });
726
821
  }
@@ -2,8 +2,8 @@ import type { Runtime } from "../client.js";
2
2
  import type { Snapshot as RuntimeSnapshot } from "../snapshots.js";
3
3
  import { type Credentials, type WithRuntime } from "./client.js";
4
4
  type Status = "created" | "failed" | "deleted";
5
- /** A saved sandbox, as Vercel's Snapshot, over a Runtime snapshot: files,
6
- * memory and running processes. `sourceSessionId` is the Runtime id of the
5
+ /** A saved filesystem, as Vercel's Snapshot, over a Runtime disk snapshot.
6
+ * Restoring it starts fresh processes. `sourceSessionId` is the Runtime id of the
7
7
  * sandbox it came from. */
8
8
  export declare class Snapshot {
9
9
  #private;
@@ -26,6 +26,7 @@ export declare class Snapshot {
26
26
  static list(params?: {
27
27
  name?: string;
28
28
  limit?: number;
29
+ signal?: AbortSignal;
29
30
  } & Credentials & {
30
31
  withruntime?: WithRuntime;
31
32
  } & Record<string, unknown>): Promise<{
@@ -20,8 +20,8 @@ function metadataOf(snapshot) {
20
20
  expiresAt: Date.parse(snapshot.expiresAt),
21
21
  };
22
22
  }
23
- /** A saved sandbox, as Vercel's Snapshot, over a Runtime snapshot: files,
24
- * memory and running processes. `sourceSessionId` is the Runtime id of the
23
+ /** A saved filesystem, as Vercel's Snapshot, over a Runtime disk snapshot.
24
+ * Restoring it starts fresh processes. `sourceSessionId` is the Runtime id of the
25
25
  * sandbox it came from. */
26
26
  export class Snapshot {
27
27
  #snapshot;
@@ -60,13 +60,14 @@ export class Snapshot {
60
60
  }
61
61
  /** Snapshots, oldest first; `name` narrows to one sandbox's. */
62
62
  static async list(params = {}) {
63
+ params.signal?.throwIfAborted();
63
64
  for (const field of ["since", "until", "cursor", "sortOrder"])
64
65
  if (params[field] !== undefined)
65
66
  throw new NotSupportedError(`Listing snapshots by ${field}`, "List them all and filter the result yourself.");
66
67
  const client = clientFor(params);
67
68
  let sandboxId;
68
69
  if (params.name !== undefined) {
69
- const found = (await guard(() => client.sandboxes.list({ name: params.name, includeStopped: true }))).data[0];
70
+ const found = (await guard(() => client.sandboxes.list({ name: params.name, includeStopped: true }, signalOf(params)))).data[0];
70
71
  if (!found)
71
72
  return paginate([], null);
72
73
  sandboxId = found.id;
@@ -74,7 +75,7 @@ export class Snapshot {
74
75
  const page = await guard(() => client.snapshots.list({
75
76
  ...(sandboxId ? { sandboxId } : {}),
76
77
  ...(params.limit ? { limit: params.limit } : {}),
77
- }));
78
+ }, signalOf(params)));
78
79
  return paginate(page.data, page.nextCursor, page);
79
80
  }
80
81
  static tree() {
@@ -83,10 +84,13 @@ export class Snapshot {
83
84
  async delete(opts = {}) {
84
85
  try {
85
86
  await guard(() => this.#client.snapshots.delete(this.#snapshot.id, signalOf(opts)));
87
+ Object.assign(this.#snapshot, await guard(() => this.#client.snapshots.get(this.#snapshot.id, signalOf(opts))));
86
88
  }
87
89
  catch (error) {
88
- if (error instanceof APIError && error.response.status === 404)
90
+ if (error instanceof APIError && error.response.status === 404) {
91
+ this.#snapshot.state = "deleted";
89
92
  return;
93
+ }
90
94
  throw error;
91
95
  }
92
96
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "withruntime",
3
- "version": "0.8.4",
3
+ "version": "0.10.0",
4
4
  "license": "Apache-2.0",
5
5
  "mcpName": "com.withruntime/runtime",
6
6
  "type": "module",
@@ -59,6 +59,41 @@
59
59
  "types": "./dist/ai-harness/index.d.ts",
60
60
  "import": "./dist/ai-harness/index.js",
61
61
  "default": "./dist/ai-harness/index.js"
62
+ },
63
+ "./runloop": {
64
+ "types": "./dist/runloop/index.d.ts",
65
+ "import": "./dist/runloop/index.js",
66
+ "default": "./dist/runloop/index.js"
67
+ },
68
+ "./codesandbox": {
69
+ "types": "./dist/codesandbox/index.d.ts",
70
+ "import": "./dist/codesandbox/index.js",
71
+ "default": "./dist/codesandbox/index.js"
72
+ },
73
+ "./sprites": {
74
+ "types": "./dist/sprites/index.d.ts",
75
+ "import": "./dist/sprites/index.js",
76
+ "default": "./dist/sprites/index.js"
77
+ },
78
+ "./freestyle": {
79
+ "types": "./dist/freestyle/index.d.ts",
80
+ "import": "./dist/freestyle/index.js",
81
+ "default": "./dist/freestyle/index.js"
82
+ },
83
+ "./modal": {
84
+ "types": "./dist/modal/index.d.ts",
85
+ "import": "./dist/modal/index.js",
86
+ "default": "./dist/modal/index.js"
87
+ },
88
+ "./cloudflare": {
89
+ "workerd": {
90
+ "types": "./dist/cloudflare/worker.d.ts",
91
+ "import": "./dist/cloudflare/worker.js",
92
+ "default": "./dist/cloudflare/worker.js"
93
+ },
94
+ "types": "./dist/cloudflare/index.d.ts",
95
+ "import": "./dist/cloudflare/index.js",
96
+ "default": "./dist/cloudflare/index.js"
62
97
  }
63
98
  },
64
99
  "bin": {
@@ -72,7 +107,9 @@
72
107
  "DAYTONA.md",
73
108
  "VERCEL.md",
74
109
  "BLAXEL.md",
75
- "LICENSE"
110
+ "LICENSE",
111
+ "COMPATIBILITY.md",
112
+ "compatibility-lock.json"
76
113
  ],
77
114
  "engines": {
78
115
  "node": ">=22.12"
@@ -86,12 +123,17 @@
86
123
  "test:java": "bun ../../sdks/java/scripts/check.ts",
87
124
  "test:ruby": "bun ../../sdks/ruby/scripts/check.ts",
88
125
  "test:python": "bun scripts/test-python.ts",
89
- "test:go": "bun scripts/test-go.ts"
126
+ "test:go": "bun scripts/test-go.ts",
127
+ "compat:upstream": "bun scripts/check-upstream.ts",
128
+ "test:compat": "bun test tests/e2b tests/daytona tests/vercel tests/blaxel tests/compat tests/drop-in-isolation.test.ts tests/drop-in-paths.test.ts tests/process-input.test.ts tests/watch-reentrant.test.ts tests/upstream-check.test.ts tests/process-output.test.ts tests/prepare-compatibility.test.ts",
129
+ "compat:prepare": "bun scripts/prepare-compatibility.ts",
130
+ "compat:oracle": "bun oracle/check.mjs"
90
131
  },
91
132
  "devDependencies": {
92
133
  "@ai-sdk/harness": "1.0.124",
93
134
  "@anthropic-ai/claude-agent-sdk": "0.3.277",
94
135
  "@anthropic-ai/sdk": "0.128.0",
136
+ "@cloudflare/workers-types": "5.20260929.1",
95
137
  "@google/genai": "2.24.0",
96
138
  "@mastra/core": "1.71.0",
97
139
  "@mistralai/mistralai": "2.7.0",
@@ -127,6 +169,7 @@
127
169
  "bugs": "https://github.com/withruntime/runtime/issues",
128
170
  "sideEffects": false,
129
171
  "browser": {
172
+ "./dist/file-source.js": "./dist/browser-stub.js",
130
173
  "./dist/tar.js": "./dist/browser-stub.js",
131
174
  "./dist/image-context.js": "./dist/browser-stub.js",
132
175
  "./dist/credentials.js": "./dist/browser-stub.js",