withruntime 0.10.0 → 0.11.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 (94) hide show
  1. package/BLAXEL.md +7 -8
  2. package/DAYTONA.md +40 -35
  3. package/E2B.md +70 -57
  4. package/README.md +3 -3
  5. package/README.src.md +3 -3
  6. package/VERCEL.md +23 -20
  7. package/compatibility-lock.json +63 -84
  8. package/dist/api-defaults.d.ts +1 -1
  9. package/dist/api-defaults.js +1 -1
  10. package/dist/blaxel/lookup.d.ts +2 -1
  11. package/dist/blaxel/lookup.js +2 -1
  12. package/dist/blaxel/sandbox.d.ts +7 -5
  13. package/dist/blaxel/sandbox.js +15 -10
  14. package/dist/cli.js +112 -17
  15. package/dist/client.d.ts +17 -1
  16. package/dist/client.js +46 -3
  17. package/dist/compat/files.js +5 -1
  18. package/dist/compat/lease.d.ts +37 -0
  19. package/dist/compat/lease.js +93 -0
  20. package/dist/credentials.d.ts +9 -0
  21. package/dist/credentials.js +26 -4
  22. package/dist/daytona/computer-use.d.ts +162 -0
  23. package/dist/daytona/computer-use.js +324 -0
  24. package/dist/daytona/context.d.ts +10 -0
  25. package/dist/daytona/context.js +24 -0
  26. package/dist/daytona/daytona.d.ts +20 -0
  27. package/dist/daytona/daytona.js +150 -26
  28. package/dist/daytona/filesystem.js +8 -0
  29. package/dist/daytona/index.d.ts +2 -26
  30. package/dist/daytona/index.js +2 -7
  31. package/dist/daytona/process.d.ts +63 -6
  32. package/dist/daytona/process.js +224 -23
  33. package/dist/daytona/sandbox.d.ts +25 -6
  34. package/dist/daytona/sandbox.js +60 -27
  35. package/dist/e2b/client.d.ts +5 -0
  36. package/dist/e2b/client.js +5 -1
  37. package/dist/e2b/commands.d.ts +15 -6
  38. package/dist/e2b/commands.js +83 -18
  39. package/dist/e2b/errors.d.ts +7 -0
  40. package/dist/e2b/errors.js +20 -0
  41. package/dist/e2b/filesystem.d.ts +2 -1
  42. package/dist/e2b/filesystem.js +43 -6
  43. package/dist/e2b/index.d.ts +2 -2
  44. package/dist/e2b/index.js +1 -1
  45. package/dist/e2b/pty.js +6 -3
  46. package/dist/e2b/sandbox.d.ts +20 -6
  47. package/dist/e2b/sandbox.js +207 -31
  48. package/dist/e2b/users.d.ts +30 -0
  49. package/dist/e2b/users.js +185 -0
  50. package/dist/errors.d.ts +6 -0
  51. package/dist/errors.js +6 -0
  52. package/dist/identity.js +2 -1
  53. package/dist/index.d.ts +3 -1
  54. package/dist/index.js +2 -1
  55. package/dist/mcp.d.ts +2 -1
  56. package/dist/mcp.js +6 -1
  57. package/dist/modal/index.js +6 -2
  58. package/dist/network-cli.js +33 -5
  59. package/dist/products/browser.d.ts +61 -0
  60. package/dist/products/browser.js +27 -0
  61. package/dist/products/desktop.d.ts +4 -3
  62. package/dist/products/desktop.js +7 -32
  63. package/dist/products/images.d.ts +3 -1
  64. package/dist/products/images.js +14 -2
  65. package/dist/products/index.d.ts +2 -0
  66. package/dist/products/index.js +2 -0
  67. package/dist/products/jobs.d.ts +7 -5
  68. package/dist/products/jobs.js +7 -3
  69. package/dist/products/mcp.d.ts +5 -1
  70. package/dist/products/mcp.js +29 -8
  71. package/dist/products/network-products.d.ts +8 -1
  72. package/dist/products/network-products.js +4 -1
  73. package/dist/products/observability.d.ts +1 -1
  74. package/dist/products/private-network.d.ts +22 -0
  75. package/dist/products/private-network.js +10 -0
  76. package/dist/products/secrets.d.ts +2 -2
  77. package/dist/runloop/index.js +6 -2
  78. package/dist/sandbox.d.ts +44 -14
  79. package/dist/sandbox.js +162 -48
  80. package/dist/tar.d.ts +2 -0
  81. package/dist/tar.js +74 -16
  82. package/dist/transport.d.ts +33 -7
  83. package/dist/transport.js +171 -36
  84. package/dist/types.d.ts +59 -11
  85. package/dist/vercel/filesystem.js +3 -1
  86. package/dist/vercel/index.d.ts +1 -7
  87. package/dist/vercel/index.js +1 -3
  88. package/dist/vercel/sandbox-user.d.ts +70 -0
  89. package/dist/vercel/sandbox-user.js +206 -0
  90. package/dist/vercel/sandbox.d.ts +57 -15
  91. package/dist/vercel/sandbox.js +219 -47
  92. package/dist/wait.d.ts +12 -0
  93. package/dist/wait.js +47 -0
  94. package/package.json +1 -1
@@ -1,8 +1,10 @@
1
+ import { extensionSeconds, LeaseKeeper, LEASE_MAX_SECONDS } from "../compat/lease.js";
1
2
  import { RuntimeError } from "../errors.js";
2
3
  import { clientFor, signalOf, } from "./client.js";
3
4
  import { Command, CommandFinished, exitCodeOf } from "./command.js";
4
5
  import { APIError, guard, NotSupportedError, translate } from "./errors.js";
5
6
  import { FileSystem } from "./filesystem.js";
7
+ import { rootOrThrow, SandboxUser, SandboxUserAlreadyExistsError, validateName, } from "./sandbox-user.js";
6
8
  import { Snapshot } from "./snapshot.js";
7
9
  /* Vercel Sandbox's `Sandbox` over Runtime's. Every call is one or a few calls
8
10
  of the withruntime SDK; nothing here speaks HTTP. What Runtime cannot do the
@@ -11,10 +13,13 @@ import { Snapshot } from "./snapshot.js";
11
13
  * 2026). Runtime gives 2 vCPU and 4096 MiB. */
12
14
  export const DEFAULT_VCPUS = 2;
13
15
  export const MEMORY_MIB_PER_VCPU = 2048;
14
- /** Vercel's default timeout, 5 minutes. */
16
+ /** Vercel's default timeout, 5 minutes. Not sent: a sandbox created with no
17
+ * timeout has no time limit on Runtime, running while it works and pausing
18
+ * when idle (0300). Kept for code that imports it. */
15
19
  export const DEFAULT_TIMEOUT_MS = 300_000;
16
20
  const MIN_LEASE_SECONDS = 60;
17
- const MAX_LEASE_SECONDS = 3600;
21
+ /** Extend a long timeout's lease when less than this is left. */
22
+ const LEASE_MARGIN_MS = 600_000;
18
23
  /** Commands without a timeout run until the sandbox ends: Runtime's longest. */
19
24
  const LONGEST_MS = 86_400_000;
20
25
  /** Vercel's working directory; `/workspace` on Runtime. */
@@ -27,13 +32,14 @@ const LEGACY_RUNTIMES = new Set(["node22", "node24", "node26", "python3.13"]);
27
32
  /** Vercel regions in the United States, where Runtime runs. */
28
33
  const US_REGIONS = new Set(["iad1", "sfo1", "cle1", "pdx1"]);
29
34
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
30
- function leaseSeconds(timeoutMs) {
35
+ function checkTimeout(timeoutMs) {
31
36
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
32
37
  throw new RangeError(`timeout must be a positive number of milliseconds, not ${timeoutMs}.`);
33
- const seconds = Math.ceil(timeoutMs / 1000);
34
- if (seconds > MAX_LEASE_SECONDS)
35
- throw new NotSupportedError(`A sandbox timeout of ${timeoutMs} ms (over one hour)`, "Runtime leases last up to an hour (3_600_000 ms); call sandbox.extendTimeout(ms) before it ends to keep going, as often as needed.");
36
- return Math.max(seconds, MIN_LEASE_SECONDS);
38
+ return Math.max(timeoutMs, MIN_LEASE_SECONDS * 1000);
39
+ }
40
+ /** The first lease for a timeout: all of it, or the hour a lease may run. */
41
+ function leaseSeconds(timeoutMs) {
42
+ return Math.min(Math.ceil(checkTimeout(timeoutMs) / 1000), LEASE_MAX_SECONDS);
37
43
  }
38
44
  function refuseCreate(params) {
39
45
  if (params.keepLastSnapshots !== undefined)
@@ -140,6 +146,12 @@ export class Sandbox {
140
146
  #persistent;
141
147
  #routes = new Map();
142
148
  #onResume;
149
+ #keeper;
150
+ /** Epoch ms the current session started, as the timeout counts from it. */
151
+ #sessionStart;
152
+ /** Vercel's timeout, when this object was given one or extended it. */
153
+ #timeoutMs;
154
+ #defaultUser;
143
155
  #linked;
144
156
  #deleted = false;
145
157
  /** Use Sandbox.create, Sandbox.get or Sandbox.getOrCreate. */
@@ -149,6 +161,17 @@ export class Sandbox {
149
161
  this.#env = { ...resolved.params.env };
150
162
  this.#persistent = resolved.params.persistent ?? resolved.runtime.info.onLeaseEnd !== "stop";
151
163
  this.#onResume = resolved.params.onResume;
164
+ const timeout = resolved.params.timeout;
165
+ this.#timeoutMs = timeout === undefined ? undefined : checkTimeout(timeout);
166
+ this.#sessionStart =
167
+ timeout === undefined
168
+ ? Date.parse(this.#rt.info.expiresAt) - this.#rt.info.timeoutSeconds * 1000
169
+ : Date.now();
170
+ this.#keeper = new LeaseKeeper({
171
+ sandbox: () => this.#rt,
172
+ until: () => this.#until(),
173
+ marginMs: () => Math.min(LEASE_MARGIN_MS, this.timeout / 2),
174
+ });
152
175
  this.fs = new FileSystem({
153
176
  name: this.name,
154
177
  files: async () => (await this.#live()).files,
@@ -213,9 +236,15 @@ export class Sandbox {
213
236
  get status() {
214
237
  return statusOf(this.#rt.state);
215
238
  }
216
- /** Milliseconds. */
239
+ /** Milliseconds from the start of the session: the timeout given at
240
+ * create, with every extension, or the time limit of a sandbox found by
241
+ * name. 0 for one with no time limit, which runs while it works. */
217
242
  get timeout() {
218
- return this.#rt.info.timeoutSeconds * 1000;
243
+ return this.#timeoutMs ?? this.#rt.info.timeoutSeconds * 1000;
244
+ }
245
+ /** Epoch ms the session should end: its start plus the timeout. */
246
+ #until() {
247
+ return this.#sessionStart + this.timeout;
219
248
  }
220
249
  get expiresAt() {
221
250
  return new Date(this.#rt.info.expiresAt);
@@ -229,7 +258,8 @@ export class Sandbox {
229
258
  }
230
259
  // ---- create, get, list ------------------------------------------------
231
260
  /** Creates a sandbox and waits until it runs. Vercel's defaults: 2 vCPUs,
232
- * 2048 MiB per vCPU, a 5-minute timeout, persistent. Funding is left to
261
+ * 2048 MiB per vCPU, persistent. With no `timeout` it has no time limit: it
262
+ * runs while it works and pauses when idle. A `timeout` is kept as given. Funding is left to
233
263
  * Runtime: the free trial while the account has trial time, then prepaid
234
264
  * credit, exactly as withruntime's own create. */
235
265
  static async create(params = {}) {
@@ -242,7 +272,8 @@ export class Sandbox {
242
272
  const source = await guard(() => resolveImage(client, params));
243
273
  const input = {
244
274
  ...(source.snapshot ? {} : { vcpu: vcpus, memoryMiB: vcpus * MEMORY_MIB_PER_VCPU }),
245
- timeoutSeconds: leaseSeconds(params.timeout ?? DEFAULT_TIMEOUT_MS),
275
+ // No timeout, no time limit: it runs while it works (0300).
276
+ ...(params.timeout === undefined ? {} : { timeoutSeconds: leaseSeconds(params.timeout) }),
246
277
  onLeaseEnd: persistent ? "pause" : "stop",
247
278
  ...(params.name ? { name: params.name } : {}),
248
279
  ...(params.tags && Object.keys(params.tags).length ? { labels: params.tags } : {}),
@@ -251,11 +282,18 @@ export class Sandbox {
251
282
  ...params.withruntime?.create,
252
283
  };
253
284
  const runtime = await guard(() => client.sandboxes.create(input, signalOf(params)));
254
- const sandbox = new Sandbox({ runtime, client, params: { ...params, persistent } });
285
+ const sandbox = new Sandbox({
286
+ runtime,
287
+ client,
288
+ params: { ...params, persistent },
289
+ });
255
290
  try {
256
291
  await sandbox.#setUp(params);
292
+ // Starts renewing at once when the timeout runs past the first lease.
293
+ await sandbox.#keeper.check();
257
294
  }
258
295
  catch (error) {
296
+ sandbox.#keeper.end();
259
297
  await runtime.stop({ wait: false }).catch(() => undefined);
260
298
  throw translate(error, sandbox.name);
261
299
  }
@@ -367,13 +405,16 @@ export class Sandbox {
367
405
  });
368
406
  }
369
407
  /** Running and stopped (paused) sandboxes, oldest first, with Vercel's
370
- * paginator: `for await (const s of await Sandbox.list())`. */
408
+ * paginator: `for await (const s of await Sandbox.list())`. A name prefix,
409
+ * creation-time bounds (`since`, `until`, epoch ms), `sortBy` and
410
+ * `sortOrder`, and a `cursor` from an earlier page are applied here, over
411
+ * every sandbox the key sees. */
371
412
  static async list(params = {}) {
372
413
  params.signal?.throwIfAborted();
373
- for (const field of ["since", "until", "cursor", "sortBy", "sortOrder", "namePrefix"])
374
- if (params[field] !== undefined)
375
- throw new NotSupportedError(`Listing sandboxes by ${field}`, "List them all (oldest first) and filter the result yourself.");
376
414
  const client = clientFor(params);
415
+ const local = ["namePrefix", "sortBy", "sortOrder", "cursor", "since", "until"].some((field) => params[field] !== undefined);
416
+ if (local)
417
+ return listHere(client, params);
377
418
  const page = await guard(() => client.sandboxes.list({
378
419
  ...(params.tags ? { labels: params.tags } : {}),
379
420
  ...(params.limit ? { limit: Math.min(params.limit, 100) } : {}),
@@ -393,10 +434,15 @@ export class Sandbox {
393
434
  const state = this.#rt.state;
394
435
  if (state === "paused" || state === "pausing")
395
436
  await this.#wake(opts);
437
+ await guard(() => this.#keeper.check(), this.name);
396
438
  return this.#rt;
397
439
  }
440
+ /** A woken sandbox starts a new session, whose timeout counts from now,
441
+ * as Vercel's resumed sessions do. */
398
442
  async #wake(opts = {}) {
399
443
  await guard(() => this.#rt.wake(signalOf(opts)), this.name);
444
+ if (this.#timeoutMs !== undefined)
445
+ this.#sessionStart = Date.now();
400
446
  await this.#onResume?.(this);
401
447
  }
402
448
  /** Runs `work` on the live sandbox; if the lease paused it meanwhile, wakes
@@ -583,6 +629,8 @@ export class Sandbox {
583
629
  else
584
630
  await guard(() => this.#rt.stop(signalOf(opts)), this.name);
585
631
  }
632
+ if (!this.#persistent)
633
+ this.#keeper.end();
586
634
  return { name: this.name, status: this.status };
587
635
  }
588
636
  /** Ends the sandbox for good. */
@@ -593,18 +641,34 @@ export class Sandbox {
593
641
  if (this.#rt.state !== "stopped")
594
642
  await guard(() => this.#rt.stop({ wait: false, ...signalOf(opts) }), this.name);
595
643
  this.#deleted = true;
644
+ this.#keeper.end();
596
645
  }
597
646
  async [Symbol.asyncDispose]() {
598
647
  if (this.#deleted)
599
648
  return;
600
649
  await this.stop().catch(() => undefined);
601
650
  }
602
- /** Moves the end `duration` milliseconds later (at most an hour ahead of
603
- * now on Runtime). */
651
+ /** Moves the end `duration` milliseconds later. The time limit moves at
652
+ * once as far as it may (an hour ahead of now); past that it is extended
653
+ * while this object lives. A sandbox with no time limit has no end to
654
+ * move, and nothing changes. */
604
655
  async extendTimeout(duration, opts = {}) {
605
656
  if (!Number.isFinite(duration) || duration <= 0)
606
657
  throw new RangeError(`duration must be a positive number of milliseconds, not ${duration}.`);
607
- await this.#withResume((runtime) => runtime.extend(Math.ceil(duration / 1000), signalOf(opts)), opts);
658
+ await this.#withResume(async (runtime) => {
659
+ if (runtime.info.endsAt === null)
660
+ return;
661
+ this.#timeoutMs = this.timeout + duration;
662
+ await this.#leaseToTimeout(runtime, opts);
663
+ }, opts);
664
+ }
665
+ /** Extends the lease now toward the timeout, an hour ahead at most. */
666
+ async #leaseToTimeout(runtime, opts) {
667
+ if (runtime.info.endsAt === null)
668
+ return;
669
+ const seconds = extensionSeconds(Date.parse(runtime.info.endsAt ?? runtime.info.expiresAt), this.#until(), Date.now(), Infinity);
670
+ if (seconds >= 1)
671
+ await runtime.extend(seconds, signalOf(opts));
608
672
  }
609
673
  /** Replaces the network rules; returns the policy given. */
610
674
  async updateNetworkPolicy(networkPolicy, opts = {}) {
@@ -615,7 +679,8 @@ export class Sandbox {
615
679
  }
616
680
  /** Changes what Runtime can change on a sandbox: `timeout`, `ports`,
617
681
  * `networkPolicy` and `snapshotExpiration`. Anything else is refused before
618
- * anything changes. */
682
+ * anything changes. A `timeout` on a sandbox with no time limit changes
683
+ * nothing: it runs while it works. */
619
684
  async update(params, opts = {}) {
620
685
  opts.signal?.throwIfAborted();
621
686
  const allowed = new Set(["timeout", "ports", "networkPolicy", "snapshotExpiration"]);
@@ -629,12 +694,19 @@ export class Sandbox {
629
694
  if (params.ports !== undefined)
630
695
  validatePorts(params.ports);
631
696
  const wanted = params.ports === undefined ? undefined : new Set(params.ports);
632
- if (params.timeout !== undefined) {
633
- const current = this.timeout;
634
- if (params.timeout < current)
635
- throw new NotSupportedError("Shortening a sandbox's timeout", "Runtime leases only move later. Call stop() when the work is done.");
636
- if (params.timeout > current)
637
- await this.extendTimeout(params.timeout - current, opts);
697
+ // A sandbox with no time limit has none to change (0300).
698
+ if (params.timeout !== undefined && this.#rt.info.endsAt !== null) {
699
+ const timeout = checkTimeout(params.timeout);
700
+ // A shorter timeout is kept when the lease has not reached it yet: the
701
+ // lease then stops being extended there. Runtime cannot end a lease
702
+ // earlier than it was given.
703
+ const end = this.#rt.info.endsAt ?? this.#rt.info.expiresAt;
704
+ if (this.#sessionStart + timeout < Date.parse(end) - 1000)
705
+ throw new NotSupportedError("A timeout that ends earlier than the current lease", `The lease runs to ${end}; call stop() when the work is done, or pass a timeout that ends after it.`);
706
+ const longer = timeout > this.timeout;
707
+ this.#timeoutMs = timeout;
708
+ if (longer)
709
+ await this.#withResume((runtime) => this.#leaseToTimeout(runtime, opts), opts);
638
710
  }
639
711
  if (rules !== undefined)
640
712
  await this.#withResume((runtime) => runtime.network.set(rules, signalOf(opts)), opts);
@@ -700,26 +772,68 @@ export class Sandbox {
700
772
  return new Snapshot(taken, this.#client);
701
773
  }
702
774
  // ---- what Runtime does differently -----------------------------------
775
+ /** Refused: Vercel hands back a token scoped to this one shell, and
776
+ * Runtime's terminal opens with the account key, which must not leave the
777
+ * server. */
703
778
  openInteractive() {
704
779
  return Promise.reject(new NotSupportedError("Vercel's interactive shell (openInteractive)", "Use `await sandbox.withruntime.terminal({ cols, rows, onData })`, or `npx withruntime sandbox shell <id>`."));
705
780
  }
706
- getDefaultUser() {
707
- return Promise.resolve({ username: "runtime", group: "runtime" });
708
- }
709
- createUser() {
710
- return usersRefused();
711
- }
712
- asUser() {
713
- throw usersError();
714
- }
715
- createGroup() {
716
- return usersRefused();
781
+ /** The user commands run as, `runtime`, and its group. */
782
+ getDefaultUser(_opts = {}) {
783
+ this.#defaultUser ??= Promise.resolve({ username: "runtime", group: "runtime" });
784
+ return this.#defaultUser;
785
+ }
786
+ /** Makes a Linux user with its own home, as Vercel does: `useradd -m`,
787
+ * then the home given to the user and the sandbox's group, mode 770. */
788
+ async createUser(username, opts = {}) {
789
+ validateName(username, "username");
790
+ const { group } = await this.getDefaultUser(opts);
791
+ const signal = opts.signal;
792
+ const useradd = await this.runCommand({
793
+ cmd: "useradd",
794
+ args: ["-m", "-s", "/bin/bash", username],
795
+ sudo: true,
796
+ ...(signal ? { signal } : {}),
797
+ });
798
+ if (useradd.exitCode !== 0) {
799
+ if (useradd.exitCode === 9)
800
+ throw new SandboxUserAlreadyExistsError(username);
801
+ throw new Error(`Failed to create user "${username}": ${await useradd.stderr()}`);
802
+ }
803
+ const home = `/home/${username}`;
804
+ await rootOrThrow(this, "chown", [`${username}:${group}`, home], signal, () => `Failed to set ownership on ${home}`);
805
+ await rootOrThrow(this, "chmod", ["770", home], signal, () => `Failed to set permissions on ${home}`);
806
+ return new SandboxUser({ sandbox: this, username });
807
+ }
808
+ /** A handle for an existing user, such as `root`. */
809
+ asUser(username) {
810
+ validateName(username, "username");
811
+ return new SandboxUser({ sandbox: this, username });
812
+ }
813
+ /** Makes a group and its shared directory, `/shared/<group>`, setgid 2770. */
814
+ async createGroup(groupname, opts = {}) {
815
+ validateName(groupname, "group name");
816
+ const { username } = await this.getDefaultUser(opts);
817
+ const sharedDir = `/shared/${groupname}`;
818
+ const steps = [
819
+ ["groupadd", [groupname], `Failed to create group "${groupname}"`],
820
+ ["mkdir", ["-p", sharedDir], `Failed to create shared directory ${sharedDir}`],
821
+ ["chown", [`${username}:${groupname}`, sharedDir], `Failed to set ownership on ${sharedDir}`],
822
+ ["chmod", ["2770", sharedDir], `Failed to set permissions on ${sharedDir}`],
823
+ ];
824
+ for (const [cmd, args, what] of steps)
825
+ await rootOrThrow(this, cmd, args, opts.signal, () => what);
826
+ return { groupname, sharedDir };
717
827
  }
718
- addUserToGroup() {
719
- return usersRefused();
828
+ async addUserToGroup(username, groupname, opts = {}) {
829
+ validateName(username, "username");
830
+ validateName(groupname, "group name");
831
+ await rootOrThrow(this, "usermod", ["-aG", groupname, username], opts.signal, () => `Failed to add "${username}" to group "${groupname}"`);
720
832
  }
721
- removeUserFromGroup() {
722
- return usersRefused();
833
+ async removeUserFromGroup(username, groupname, opts = {}) {
834
+ validateName(username, "username");
835
+ validateName(groupname, "group name");
836
+ await rootOrThrow(this, "gpasswd", ["-d", username, groupname], opts.signal, () => `Failed to remove "${username}" from group "${groupname}"`);
723
837
  }
724
838
  currentSession() {
725
839
  throw new NotSupportedError("Vercel sessions (currentSession)", "A Runtime sandbox is its own session: call the methods on the sandbox.");
@@ -731,12 +845,6 @@ export class Sandbox {
731
845
  return Promise.reject(new NotSupportedError("Listing a sandbox's snapshots from the sandbox", "Use Snapshot.list({ name }) or runtime.snapshots.list({ sandboxId })."));
732
846
  }
733
847
  }
734
- function usersError() {
735
- return new NotSupportedError("Extra Linux users and groups (createUser, asUser, groups)", "Commands run as the sandbox owner with passwordless sudo; run `sudo useradd ...` and `sudo -u <user> ...` with runCommand.");
736
- }
737
- function usersRefused() {
738
- return Promise.reject(usersError());
739
- }
740
848
  /** Milliseconds as whole days for Runtime's retention: 1 to 365, and 0 (no
741
849
  * expiration) as 365. */
742
850
  export function retentionDays(ms) {
@@ -791,6 +899,70 @@ function itemOf(runtime) {
791
899
  tags: { ...info.labels },
792
900
  };
793
901
  }
902
+ const CURSOR = /^rt\.([0-9]+)$/;
903
+ /** Sandbox.list with a name prefix, time bounds, sorting or a cursor:
904
+ * applied here over every sandbox the key sees, since Runtime's list filters
905
+ * only by labels and name. The cursor is this adapter's own, an offset into
906
+ * the same query. */
907
+ async function listHere(client, params) {
908
+ let offset = 0;
909
+ if (params.cursor !== undefined) {
910
+ const match = CURSOR.exec(params.cursor);
911
+ if (!match)
912
+ throw new RangeError(`The cursor "${params.cursor}" was not made by Sandbox.list here; pass pagination.next from an earlier page of the same query.`);
913
+ offset = Number(match[1]);
914
+ }
915
+ if (params.sortBy !== undefined &&
916
+ !["createdAt", "name", "statusUpdatedAt"].includes(params.sortBy))
917
+ throw new RangeError("sortBy must be createdAt, name or statusUpdatedAt.");
918
+ if (params.sortOrder !== undefined && !["asc", "desc"].includes(params.sortOrder))
919
+ throw new RangeError("sortOrder must be asc or desc.");
920
+ const page = await guard(() => client.sandboxes.list(params.tags ? { labels: params.tags } : {}, signalOf(params)));
921
+ const all = (await guard(() => page.toArray(10_000))).map(itemOf);
922
+ params.signal?.throwIfAborted();
923
+ const time = (value) => value === undefined ? undefined : value instanceof Date ? value.getTime() : value;
924
+ const since = time(params.since);
925
+ const until = time(params.until);
926
+ const items = all.filter((one) => (params.namePrefix === undefined || one.name.startsWith(params.namePrefix)) &&
927
+ (since === undefined || one.createdAt >= since) &&
928
+ (until === undefined || one.createdAt <= until));
929
+ const by = params.sortBy ?? "createdAt";
930
+ const key = (one) => by === "name" ? one.name : by === "statusUpdatedAt" ? one.updatedAt : one.createdAt;
931
+ const direction = params.sortOrder === "desc" ? -1 : 1;
932
+ items.sort((a, b) => {
933
+ const [x, y] = [key(a), key(b)];
934
+ return (x < y ? -1 : x > y ? 1 : 0) * direction;
935
+ });
936
+ const size = Math.max(1, Math.min(params.limit ?? 50, 100));
937
+ const pageAt = (from) => {
938
+ const sandboxes = items.slice(from, from + size);
939
+ return {
940
+ sandboxes,
941
+ pagination: {
942
+ count: sandboxes.length,
943
+ next: from + size < items.length ? `rt.${from + size}` : null,
944
+ },
945
+ };
946
+ };
947
+ return Object.assign(pageAt(offset), {
948
+ async *pages() {
949
+ for (let from = offset; from === offset || from < items.length; from += size) {
950
+ params.signal?.throwIfAborted();
951
+ yield pageAt(from);
952
+ }
953
+ },
954
+ async *[Symbol.asyncIterator]() {
955
+ for (const item of items.slice(offset)) {
956
+ params.signal?.throwIfAborted();
957
+ yield item;
958
+ }
959
+ },
960
+ async toArray() {
961
+ params.signal?.throwIfAborted();
962
+ return items.slice(offset);
963
+ },
964
+ });
965
+ }
794
966
  /** Vercel's paginator over a Runtime page: the first page's fields, async
795
967
  * iteration over every item, `pages()` and `toArray()`. */
796
968
  function paginate(first, signal) {
package/dist/wait.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import type { RequestOptions } from "./transport.js";
2
+ /** A wait that an abort ends at once, rejecting with the signal's reason. */
3
+ export declare function pause(ms: number, signal?: AbortSignal): Promise<void>;
4
+ export declare function abortError(signal: AbortSignal): Error;
5
+ /**
6
+ * Calls `attempt` until it stops answering one of `codes` (something is
7
+ * still installing), within one budget: the caller's `timeoutMs`, else ten
8
+ * minutes. Each try gets what is left of it; a wait between tries ends at once
9
+ * on abort, and no try starts once the budget cannot cover the wait the API
10
+ * asked for: the installing error is thrown instead.
11
+ */
12
+ export declare function whileInstalling<T>(codes: readonly string[], options: RequestOptions | undefined, attempt: (options: RequestOptions) => Promise<T>): Promise<T>;
package/dist/wait.js ADDED
@@ -0,0 +1,47 @@
1
+ /** A wait that an abort ends at once, rejecting with the signal's reason. */
2
+ export function pause(ms, signal) {
3
+ return new Promise((resolve, reject) => {
4
+ if (signal?.aborted)
5
+ return reject(abortError(signal));
6
+ const timer = setTimeout(() => {
7
+ signal?.removeEventListener("abort", stop);
8
+ resolve();
9
+ }, ms);
10
+ function stop() {
11
+ clearTimeout(timer);
12
+ reject(abortError(signal));
13
+ }
14
+ signal?.addEventListener("abort", stop, { once: true });
15
+ });
16
+ }
17
+ export function abortError(signal) {
18
+ return signal.reason instanceof Error ? signal.reason : new Error("Aborted");
19
+ }
20
+ /** How long a start waits for a first install (Chromium, the desktop) when
21
+ * the caller sets no deadline. */
22
+ const INSTALL_WAIT_MS = 600_000;
23
+ /**
24
+ * Calls `attempt` until it stops answering one of `codes` (something is
25
+ * still installing), within one budget: the caller's `timeoutMs`, else ten
26
+ * minutes. Each try gets what is left of it; a wait between tries ends at once
27
+ * on abort, and no try starts once the budget cannot cover the wait the API
28
+ * asked for: the installing error is thrown instead.
29
+ */
30
+ export async function whileInstalling(codes, options, attempt) {
31
+ const deadline = performance.now() + (options?.timeoutMs || INSTALL_WAIT_MS);
32
+ for (;;) {
33
+ if (options?.signal?.aborted)
34
+ throw abortError(options.signal);
35
+ const left = Math.max(1, Math.ceil(deadline - performance.now()));
36
+ try {
37
+ return await attempt(options?.timeoutMs ? { ...options, timeoutMs: left } : { ...options });
38
+ }
39
+ catch (error) {
40
+ const code = error.code;
41
+ const wait = error.retryAfterMs ?? 10_000;
42
+ if (!code || !codes.includes(code) || performance.now() + wait > deadline)
43
+ throw error;
44
+ await pause(wait, options?.signal);
45
+ }
46
+ }
47
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "withruntime",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "license": "Apache-2.0",
5
5
  "mcpName": "com.withruntime/runtime",
6
6
  "type": "module",