withruntime 0.11.0 → 0.11.2

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 (84) hide show
  1. package/BLAXEL.md +8 -9
  2. package/DAYTONA.md +8 -9
  3. package/E2B.md +63 -21
  4. package/README.md +6 -5
  5. package/README.src.md +6 -5
  6. package/VERCEL.md +3 -2
  7. package/compatibility-lock.json +69 -14
  8. package/dist/api-defaults.d.ts +1 -1
  9. package/dist/api-defaults.js +1 -1
  10. package/dist/blaxel/errors.js +4 -4
  11. package/dist/blaxel/lookup.d.ts +2 -1
  12. package/dist/blaxel/lookup.js +2 -1
  13. package/dist/blaxel/sandbox.d.ts +8 -7
  14. package/dist/blaxel/sandbox.js +16 -12
  15. package/dist/cli.d.ts +1 -1
  16. package/dist/cli.js +99 -27
  17. package/dist/client.d.ts +16 -60
  18. package/dist/client.js +14 -103
  19. package/dist/compat/files.js +5 -1
  20. package/dist/compat/lease.d.ts +1 -0
  21. package/dist/compat/lease.js +14 -8
  22. package/dist/credentials.d.ts +9 -0
  23. package/dist/credentials.js +26 -4
  24. package/dist/daytona/daytona.d.ts +1 -2
  25. package/dist/daytona/daytona.js +30 -14
  26. package/dist/daytona/sandbox.d.ts +9 -4
  27. package/dist/daytona/sandbox.js +24 -5
  28. package/dist/e2b/client.d.ts +15 -2
  29. package/dist/e2b/client.js +61 -7
  30. package/dist/e2b/commands.d.ts +3 -0
  31. package/dist/e2b/commands.js +16 -5
  32. package/dist/e2b/errors.d.ts +26 -2
  33. package/dist/e2b/errors.js +38 -5
  34. package/dist/e2b/filesystem.d.ts +1 -1
  35. package/dist/e2b/filesystem.js +82 -16
  36. package/dist/e2b/index.d.ts +2 -2
  37. package/dist/e2b/index.js +2 -2
  38. package/dist/e2b/sandbox.d.ts +25 -12
  39. package/dist/e2b/sandbox.js +196 -165
  40. package/dist/e2b/unsupported.d.ts +19 -0
  41. package/dist/e2b/unsupported.js +21 -0
  42. package/dist/e2b/users.d.ts +9 -0
  43. package/dist/e2b/users.js +22 -0
  44. package/dist/errors.d.ts +10 -4
  45. package/dist/errors.js +18 -10
  46. package/dist/identity.js +2 -1
  47. package/dist/index.d.ts +2 -1
  48. package/dist/index.js +2 -1
  49. package/dist/machine.d.ts +180 -0
  50. package/dist/machine.js +504 -0
  51. package/dist/modal/index.js +6 -2
  52. package/dist/network-cli.js +7 -7
  53. package/dist/openai-agents/index.d.ts +1 -1
  54. package/dist/openai-agents/index.js +1 -1
  55. package/dist/products/browser.d.ts +2 -1
  56. package/dist/products/browser.js +9 -19
  57. package/dist/products/desktop.d.ts +2 -1
  58. package/dist/products/desktop.js +5 -30
  59. package/dist/products/jobs.d.ts +6 -5
  60. package/dist/products/jobs.js +6 -3
  61. package/dist/products/limits.d.ts +4 -10
  62. package/dist/products/mcp.d.ts +5 -1
  63. package/dist/products/mcp.js +29 -8
  64. package/dist/products/network-products.d.ts +1 -1
  65. package/dist/products/network-products.js +1 -1
  66. package/dist/products/network.d.ts +1 -1
  67. package/dist/products/observability.d.ts +1 -1
  68. package/dist/products/private-network.d.ts +1 -1
  69. package/dist/products/secrets.d.ts +3 -3
  70. package/dist/products/switching.d.ts +2 -3
  71. package/dist/runloop/index.js +6 -2
  72. package/dist/sandbox.d.ts +7 -84
  73. package/dist/sandbox.js +58 -345
  74. package/dist/tar.d.ts +2 -0
  75. package/dist/tar.js +74 -16
  76. package/dist/transport.d.ts +36 -8
  77. package/dist/transport.js +174 -37
  78. package/dist/types.d.ts +102 -12
  79. package/dist/vercel/filesystem.js +3 -1
  80. package/dist/vercel/sandbox.d.ts +18 -12
  81. package/dist/vercel/sandbox.js +27 -15
  82. package/dist/wait.d.ts +12 -0
  83. package/dist/wait.js +47 -0
  84. package/package.json +1 -1
@@ -0,0 +1,504 @@
1
+ /* The machine engine's client (CLOUD.md section 2, engines/machine/ENGINE.md):
2
+ what every product that runs code shares, written once. A product is a
3
+ setting of the engine, here the path the API serves it under, and its own
4
+ class adds what only it does: `Sandbox` extends `Machine` with commands,
5
+ files and processes, and `Sandboxes` extends `Machines`. Sandboxes are the
6
+ only machine product today; a second one is a new subclass, not a copy.
7
+ Nothing here is exported from the package: its public surface is the
8
+ product classes. */
9
+ import { KEEP_ALIVE_MARGIN_SECONDS, WAIT_FOR_TIMEOUT_SECONDS } from "./api-defaults.js";
10
+ import { RuntimeError } from "./errors.js";
11
+ import { Page } from "./page.js";
12
+ /** How long snapshot() waits, pause and capture together, unless told. */
13
+ const SNAPSHOT_DEADLINE_MS = 10 * 60_000;
14
+ const enc = (id) => encodeURIComponent(id);
15
+ /** Any other call's request options, as the caller gave them: `timeoutMs` is
16
+ * the call's whole deadline, and 0 turns it off. */
17
+ export function requestOnly(options) {
18
+ return {
19
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
20
+ ...(options.signal ? { signal: options.signal } : {}),
21
+ ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
22
+ };
23
+ }
24
+ /** DELETE /v1/<plural>/{id}, for `machine.delete()` and the product's
25
+ * `delete(id)`. */
26
+ export function deleteMachine(product, t, id, options = {}) {
27
+ return t.json({
28
+ method: "DELETE",
29
+ path: `/v1/${product.plural}/${enc(id)}`,
30
+ ...options,
31
+ });
32
+ }
33
+ /** One page of a product's list, and the pages after it. */
34
+ export function machinePage(product, t, wrap, body, query, options = {}) {
35
+ return new Page(body.data.map((info) => wrap(t, info)), body.nextCursor, async (cursor) => machinePage(product, t, wrap, await t.json({
36
+ method: "GET",
37
+ path: `/v1/${product.plural}`,
38
+ query: Object.assign({}, query, { cursor }),
39
+ ...options,
40
+ }), query, options));
41
+ }
42
+ /** One machine: its record and the verbs every machine product shares. */
43
+ export class Machine {
44
+ #info;
45
+ #keepAlive;
46
+ #t;
47
+ #product;
48
+ constructor(product, transport, info) {
49
+ this.#product = product;
50
+ this.#t = transport;
51
+ this.#info = info;
52
+ }
53
+ get id() {
54
+ return this.#info.id;
55
+ }
56
+ /** The API's latest answer. `start`, the report of an image's start
57
+ * command, is the create's alone: the API keeps no record of it, so a
58
+ * later read of this sandbox keeps it here. */
59
+ #keep(info) {
60
+ const start = this.#info?.start;
61
+ this.#info = info.start === undefined && start !== undefined ? { ...info, start } : info;
62
+ }
63
+ #path(suffix = "") {
64
+ return `/v1/${this.#product.plural}/${enc(this.id)}${suffix}`;
65
+ }
66
+ /** What the API last said about this sandbox. `refresh()` asks again. */
67
+ get info() {
68
+ return this.#info;
69
+ }
70
+ get state() {
71
+ return this.#info.state;
72
+ }
73
+ async refresh(options = {}) {
74
+ this.#keep(await this.#t.json({
75
+ method: "GET",
76
+ path: this.#path(),
77
+ ...options,
78
+ }));
79
+ return this;
80
+ }
81
+ /** Waits (server-side, no polling) until the sandbox reaches `state`. */
82
+ async waitFor(state, options = {}) {
83
+ this.#keep(await this.#t.json({
84
+ method: "GET",
85
+ path: this.#path(),
86
+ query: {
87
+ waitFor: state,
88
+ timeoutSeconds: options.timeoutSeconds ?? WAIT_FOR_TIMEOUT_SECONDS,
89
+ },
90
+ ...options,
91
+ }));
92
+ return this;
93
+ }
94
+ async stop(options = {}) {
95
+ this.#keepAlive?.();
96
+ return this.#lifecycle("stop", options);
97
+ }
98
+ async pause(options = {}) {
99
+ return this.#lifecycle("pause", options);
100
+ }
101
+ /** Deletes it for good: stops it if it runs or is paused, deletes its disk
102
+ * and paused memory, revokes its previews and ports, and removes it from
103
+ * lists. Its snapshots, usage and audit entries stay. Deleting it again
104
+ * answers the same. */
105
+ async delete(options = {}) {
106
+ this.#keepAlive?.();
107
+ return deleteMachine(this.#product, this.#t, this.id, options);
108
+ }
109
+ /** Carries on a paused sandbox; for one with a time limit,
110
+ * `timeoutSeconds` is its new one. */
111
+ async wake(options = {}) {
112
+ const { timeoutSeconds, ...rest } = options;
113
+ try {
114
+ return await this.#lifecycle("wake", rest, timeoutSeconds === undefined ? {} : { timeoutSeconds });
115
+ }
116
+ catch (error) {
117
+ // Waking one that is already awake is done, not a mistake. With a new
118
+ // lease asked for, the refusal stands: it was not given.
119
+ if (!(error instanceof RuntimeError) || error.code !== "not_paused" || timeoutSeconds)
120
+ throw error;
121
+ await this.refresh(requestOnly(rest));
122
+ if (this.state !== "running" && this.state !== "starting")
123
+ throw error;
124
+ return this;
125
+ }
126
+ }
127
+ /** More time before its time limit ends (at most an hour ahead of now). A
128
+ * sandbox with no time limit needs none: the call answers at once and
129
+ * changes nothing. */
130
+ async extend(seconds, options = {}) {
131
+ return this.#lifecycle("extend", { ...options, wait: false }, { seconds });
132
+ }
133
+ /** Changes its name, labels, automatic wake, idle pause or persistence;
134
+ * fields left out stay as they are. `persistent: true` keeps it running
135
+ * while credit lasts (paid only). */
136
+ async update(settings, options = {}) {
137
+ return this.#lifecycle("update", { ...options, wait: false }, { ...settings });
138
+ }
139
+ /** Moves it to another image (id, name, name:tag or name@version), keeping
140
+ * its id, /workspace (its home: dotfiles, pip --user and npm -g installs),
141
+ * volumes, environment, name and previews. Its processes restart, and
142
+ * everything else on its old disk (sudo installs, apt packages, /etc) is
143
+ * lost; snapshot it first to keep everything. A running sandbox is paused
144
+ * first. A switch that fails is undone (`switch_undone`), the sandbox on
145
+ * its old image with nothing lost. Charged as a wake. `keep` is optional:
146
+ * "workspace" is the only value, and it is sent either way, so an API from
147
+ * before it was optional accepts the call too. */
148
+ async switchImage(image, options = {}) {
149
+ const { keep = "workspace", ...rest } = options;
150
+ this.#keep(await this.#t.json({
151
+ method: "POST",
152
+ path: this.#path(":switch-image"),
153
+ body: { image, keep },
154
+ // A pause, two boots and the copy between them.
155
+ wait: 120,
156
+ ...requestOnly(rest),
157
+ }));
158
+ return this;
159
+ }
160
+ /** Gives it more or fewer vCPUs or more or less memory by a restart: its id,
161
+ * whole disk, volumes, environment, name and previews stay, and its
162
+ * programs stop (snapshot it first to keep its memory too). A running sandbox is paused, or stopped if it is
163
+ * persistent, and comes back running at the new size on the same server; a
164
+ * paused or stopped one is started. Memory bills on the new size from then.
165
+ * Refused, with nothing changed, when the server has no room
166
+ * (`no_capacity`), above your quota or the 2 vCPU and 4 GiB without credit, or
167
+ * while a snapshot or fork of it is being taken. `restart` is optional:
168
+ * true is the only value, and it is sent either way, so an API from before
169
+ * it was optional accepts the call too. */
170
+ async resize(size, options = {}) {
171
+ const { restart = true, ...rest } = options;
172
+ this.#keep(await this.#t.json({
173
+ method: "POST",
174
+ path: this.#path(":resize"),
175
+ body: { ...size, restart },
176
+ // A pause or stop and a cold boot.
177
+ wait: 120,
178
+ ...requestOnly(rest),
179
+ }));
180
+ return this;
181
+ }
182
+ /** Keeps a sandbox with a time limit running past it, in the background,
183
+ * until stop() or the returned function ends it: every `everySeconds` (60)
184
+ * it extends the limit so that `marginSeconds` (600) remain, never more
185
+ * than the hour ahead the API allows. A sandbox with no time limit
186
+ * (`endsAt` null) needs none, and is only watched. Running time is billed
187
+ * as it is used. A paused sandbox is left paused (a request wakes it,
188
+ * unless autoWake is off); a stopped one ends the loop. It does not keep a
189
+ * Node or Bun process alive by itself. */
190
+ keepAlive(options = {}) {
191
+ this.#keepAlive?.();
192
+ const every = Math.max(10, options.everySeconds ?? 60) * 1000;
193
+ const margin = Math.min(3600, Math.max(60, options.marginSeconds ?? KEEP_ALIVE_MARGIN_SECONDS));
194
+ let ended = false;
195
+ let timer;
196
+ const end = () => {
197
+ ended = true;
198
+ if (timer)
199
+ clearTimeout(timer);
200
+ if (this.#keepAlive === end)
201
+ this.#keepAlive = undefined;
202
+ };
203
+ const tick = async () => {
204
+ if (ended)
205
+ return;
206
+ try {
207
+ await this.refresh();
208
+ if (this.state === "stopped" || this.state === "stopping")
209
+ return end();
210
+ // No time limit: nothing to extend (an older server sends no endsAt).
211
+ if (this.state === "running" && this.info.endsAt !== null) {
212
+ const left = (Date.parse(this.info.endsAt ?? this.info.expiresAt) - Date.now()) / 1000;
213
+ const need = Math.ceil(margin - left);
214
+ if (need >= 1)
215
+ await this.extend(Math.min(3600, need));
216
+ }
217
+ }
218
+ catch (error) {
219
+ options.onError?.(error);
220
+ }
221
+ if (ended)
222
+ return;
223
+ timer = setTimeout(() => void tick(), every);
224
+ timer.unref?.();
225
+ };
226
+ this.#keepAlive = end;
227
+ void tick();
228
+ return end;
229
+ }
230
+ /** Days a paused sandbox is kept before deletion (1 to 365). */
231
+ async setRetention(days, options = {}) {
232
+ return this.#lifecycle("retention", { ...options, wait: false }, { days });
233
+ }
234
+ /** Keeps this sandbox's whole machine (files, memory, running processes) as
235
+ * a snapshot to start new sandboxes from. A running sandbox is paused for
236
+ * the moment it takes, then woken; a paused one stays paused. Past
237
+ * `timeoutMs` (ten minutes unless given) mid-capture, it stays paused and
238
+ * the error names the snapshot. */
239
+ async snapshot(options = {}) {
240
+ const { idempotencyKey, signal, timeoutMs, ...body } = options;
241
+ if (body.mode !== undefined && body.mode !== "memory" && body.mode !== "disk")
242
+ throw new TypeError("Snapshot mode must be memory or disk.");
243
+ signal?.throwIfAborted();
244
+ // One minute, whatever `timeoutMs` said, was too short: pausing a busy
245
+ // 16 vCPU, 32 GiB sandbox writes its memory out first (4 October 2026).
246
+ const until = performance.now() + (timeoutMs && timeoutMs > 0 ? timeoutMs : SNAPSHOT_DEADLINE_MS);
247
+ let running = false;
248
+ /** The capture in progress, once the server has one. */
249
+ let capturing;
250
+ let late = false;
251
+ const request = () => {
252
+ signal?.throwIfAborted();
253
+ const left = Math.ceil(until - performance.now());
254
+ if (left <= 0 && capturing) {
255
+ late = true;
256
+ throw new RuntimeError({
257
+ code: "snapshot_timeout",
258
+ status: 0,
259
+ message: `Snapshot ${capturing} was still capturing at the deadline.${running ? ` The ${this.#product.noun.toLowerCase()} stays paused until it ends: waking it now would fail the capture.` : ""}`,
260
+ hint: `Wait until runtime.snapshots.get("${capturing}") is ready${running ? `, then wake the ${this.#product.noun.toLowerCase()}` : ""}, or pass a longer timeoutMs.`,
261
+ details: { snapshotId: capturing, sourceSandboxId: this.id },
262
+ });
263
+ }
264
+ if (left <= 0)
265
+ throw new RuntimeError({
266
+ code: "snapshot_timeout",
267
+ status: 0,
268
+ message: "Snapshot capture ran past its deadline.",
269
+ });
270
+ return { ...(signal ? { signal } : {}), timeoutMs: left };
271
+ };
272
+ await this.refresh(request());
273
+ // Straight after a fork or a wake the sandbox is still `resuming`, and
274
+ // after a pause still `pausing`: wait for where it is going, or a
275
+ // snapshot of it is refused as not paused (user lane, 23 September 2026).
276
+ if (this.state === "resuming" || this.state === "starting")
277
+ await this.waitFor("running", request());
278
+ else if (this.state === "pausing")
279
+ await this.waitFor("paused", request());
280
+ running = this.state === "running";
281
+ // Check before entering cleanup: an abort during refresh must not pause or wake.
282
+ const pauseOptions = running ? request() : undefined;
283
+ let captured;
284
+ let primary;
285
+ let failed = false;
286
+ try {
287
+ if (running)
288
+ await this.pause(pauseOptions);
289
+ let snapshot = await this.#t.json({
290
+ method: "POST",
291
+ path: this.#path(":snapshot"),
292
+ body,
293
+ wait: 10,
294
+ ...(idempotencyKey ? { idempotencyKey } : {}),
295
+ ...request(),
296
+ });
297
+ captured = snapshot;
298
+ // Prefer: wait is bounded on the server. A capture still in progress
299
+ // must keep its source paused, or the worker refuses it after we wake.
300
+ while (snapshot.state === "capturing") {
301
+ capturing = snapshot.id;
302
+ request();
303
+ await new Promise((resolve) => setTimeout(resolve, 200));
304
+ snapshot = await this.#t.json({
305
+ method: "GET",
306
+ path: `/v1/snapshots/${enc(snapshot.id)}`,
307
+ ...request(),
308
+ });
309
+ }
310
+ if (snapshot.state !== "ready")
311
+ throw new RuntimeError({
312
+ code: "snapshot_failed",
313
+ status: 409,
314
+ message: snapshot.error ?? `Snapshot capture ended in state ${snapshot.state}.`,
315
+ details: { snapshotId: snapshot.id },
316
+ });
317
+ if (body.mode === "disk" && snapshot.mode !== "disk")
318
+ throw new RuntimeError({
319
+ code: "snapshot_mode_mismatch",
320
+ status: 409,
321
+ message: "The server did not confirm a disk-only snapshot.",
322
+ details: { snapshotId: snapshot.id },
323
+ });
324
+ captured = snapshot;
325
+ }
326
+ catch (error) {
327
+ failed = true;
328
+ primary = error;
329
+ // A request cut off by the deadline mid-capture is late all the same.
330
+ if (capturing && performance.now() >= until)
331
+ late = true;
332
+ }
333
+ // A capture still running needs its source paused: the worker fails one
334
+ // whose source woke. The timeout says so and leaves the wake to the caller.
335
+ if (running && !late) {
336
+ try {
337
+ await this.wake();
338
+ }
339
+ catch (wakeError) {
340
+ const recovery = {
341
+ ...(captured ? { snapshotId: captured.id } : {}),
342
+ sourceSandboxId: this.id,
343
+ sourceWakeError: {
344
+ ...(wakeError instanceof RuntimeError ? { code: wakeError.code } : {}),
345
+ message: wakeError instanceof Error ? wakeError.message : String(wakeError),
346
+ },
347
+ };
348
+ const reported = failed ? primary : wakeError;
349
+ if (reported instanceof Error && Object.isExtensible(reported)) {
350
+ if (reported instanceof RuntimeError)
351
+ Object.assign(reported, { details: { ...reported.details, ...recovery } });
352
+ else
353
+ Object.assign(reported, recovery);
354
+ }
355
+ if (!failed)
356
+ throw wakeError;
357
+ }
358
+ }
359
+ if (failed)
360
+ throw primary;
361
+ return captured;
362
+ }
363
+ /** Starts a stopped persistent sandbox again from its disk (memory is not kept). */
364
+ async restart(options = {}) {
365
+ return this.#lifecycle("restart", options);
366
+ }
367
+ async #lifecycle(verb, options, body = {}) {
368
+ this.#keep(await this.#t.json({
369
+ method: "POST",
370
+ path: this.#path(`:${verb}`),
371
+ body,
372
+ wait: options.wait === false ? 0 : 60,
373
+ ...requestOnly(options),
374
+ }));
375
+ return this;
376
+ }
377
+ async [Symbol.asyncDispose]() {
378
+ this.#keepAlive?.();
379
+ if (this.#info.state === "stopped")
380
+ return;
381
+ await this.stop({ wait: false }).catch(() => undefined);
382
+ }
383
+ toJSON() {
384
+ return this.#info;
385
+ }
386
+ }
387
+ /** A machine product's collection: `runtime.<product>`. */
388
+ export class Machines {
389
+ #t;
390
+ #product;
391
+ #wrap;
392
+ constructor(product, t, wrap) {
393
+ this.#product = product;
394
+ this.#t = t;
395
+ this.#wrap = wrap;
396
+ }
397
+ /** Creates a sandbox and waits until it is running. Every field is optional:
398
+ * with none you get the included usage while it lasts, the default region and a
399
+ * 2 vCPU / 4 GiB machine from a warm template. `wait: false` returns at once.
400
+ * When every slot without credit or the account's quota is taken, it waits for one to
401
+ * free, up to `waitForCapacityMs` (the client's, two minutes by default). */
402
+ async create(input = {}, options = {}) {
403
+ const { wait, ...body } = input;
404
+ const began = performance.now();
405
+ // Time spent waiting for capacity is the room's, not the deadline's.
406
+ let waited = 0;
407
+ const info = await this.#t.json({
408
+ method: "POST",
409
+ path: `/v1/${this.#product.plural}`,
410
+ body,
411
+ wait: wait === false ? 0 : 60,
412
+ ...options,
413
+ waitForCapacityMs: options.waitForCapacityMs ?? this.#t.waitForCapacityMs,
414
+ onCapacityWait: (refusal, waitMs) => {
415
+ waited += waitMs;
416
+ options.onCapacityWait?.(refusal, waitMs);
417
+ },
418
+ });
419
+ const machine = this.#wrap(this.#t, info);
420
+ if (wait !== false && info.state !== "running") {
421
+ // One deadline for the whole create: the wait for running gets what the
422
+ // create left of it (1 ms when nothing is left, so it ends as a timeout).
423
+ const { timeoutMs } = options;
424
+ await machine.waitFor("running", {
425
+ timeoutSeconds: WAIT_FOR_TIMEOUT_SECONDS,
426
+ signal: options.signal,
427
+ timeoutMs: timeoutMs
428
+ ? Math.max(1, Math.ceil(timeoutMs - (performance.now() - began - waited)))
429
+ : timeoutMs,
430
+ idempotencyKey: options.idempotencyKey,
431
+ });
432
+ if (machine.state !== "running")
433
+ throw new RuntimeError({
434
+ message: `${this.#product.noun} ${info.id} is ${machine.state}, not running.`,
435
+ code: "start_failed",
436
+ status: 0,
437
+ hint: `Read it with runtime.${this.#product.plural}.get(id); stopReason says why.`,
438
+ });
439
+ }
440
+ return machine;
441
+ }
442
+ /** The sandbox named `name` in this account, ready to use: running as it
443
+ * is, woken if it is paused, restarted if it is stopped and persistent, or
444
+ * created with `input` when no sandbox has the name. `sandbox.info.reused`
445
+ * says which. The other fields apply only when it is created. */
446
+ async getOrCreate(name, input = {}, options = {}) {
447
+ return this.create({ ...input, name, getOrCreate: true }, options);
448
+ }
449
+ async get(id, options = {}) {
450
+ return this.#wrap(this.#t, await this.#t.json({
451
+ method: "GET",
452
+ path: `/v1/${this.#product.plural}/${encodeURIComponent(id)}`,
453
+ ...options,
454
+ }));
455
+ }
456
+ /** Deletes a sandbox for good, by id, without reading it first: see
457
+ * `sandbox.delete()`. Deleting it again answers the same. */
458
+ delete(id, options = {}) {
459
+ return deleteMachine(this.#product, this.#t, id, options);
460
+ }
461
+ /** Live sandboxes, oldest first. Await for a page, or `for await` over all.
462
+ * Request options apply to the initial request and every subsequent page. */
463
+ async list(filter = {}, options = {}) {
464
+ const query = {
465
+ ...(filter.state ? { state: filter.state } : {}),
466
+ ...(filter.includeStopped ? { includeStopped: true } : {}),
467
+ ...(filter.labels
468
+ ? { label: Object.entries(filter.labels).map(([k, v]) => `${k}:${v}`) }
469
+ : {}),
470
+ ...(filter.name ? { name: filter.name } : {}),
471
+ ...(filter.limit ? { limit: filter.limit } : {}),
472
+ };
473
+ return machinePage(this.#product, this.#t, this.#wrap, await this.#t.json({ method: "GET", path: `/v1/${this.#product.plural}`, query, ...options }), query, options);
474
+ }
475
+ /** Stops every live sandbox whose labels all match, eight at a time, and
476
+ * says which stopped and which failed; one failure does not stop the rest.
477
+ * Needs at least one label, so stopping everything is never one call. */
478
+ async stopAll(filter, options = {}) {
479
+ if (!filter?.labels || Object.keys(filter.labels).length === 0) {
480
+ const one = this.#product.noun.toLowerCase();
481
+ throw new RuntimeError({
482
+ message: "stopAll needs at least one label to match.",
483
+ code: "invalid_request",
484
+ status: 0,
485
+ hint: `Stop one ${one} with ${one}.stop(), or label the ones to stop together.`,
486
+ });
487
+ }
488
+ const live = await (await this.list({ labels: filter.labels, limit: 100 }, options)).toArray(Infinity);
489
+ // One key cannot name several stops: each stop makes its own.
490
+ const { idempotencyKey: _one, ...each } = options;
491
+ // In list order, oldest first, whichever finishes first.
492
+ const errors = new Array(live.length);
493
+ let next = 0;
494
+ const worker = async () => {
495
+ for (let at = next++; at < live.length; at = next++)
496
+ await live[at].stop(each).catch((error) => (errors[at] = error ?? "failed"));
497
+ };
498
+ await Promise.all(Array.from({ length: Math.min(8, live.length) }, worker));
499
+ return {
500
+ stopped: live.filter((_, at) => errors[at] === undefined).map((m) => m.id),
501
+ failed: live.flatMap((m, at) => errors[at] === undefined ? [] : [{ id: m.id, error: errors[at] }]),
502
+ };
503
+ }
504
+ }
@@ -164,8 +164,12 @@ export class SandboxService {
164
164
  vcpu: params.cpu,
165
165
  memoryMiB: params.memoryMiB,
166
166
  region: placement,
167
- timeoutSeconds: Math.ceil((params.timeoutMs ?? 300_000) / 1000),
168
- idlePauseSeconds: 0,
167
+ // A timeout is the customer's limit, and the sandbox never pauses
168
+ // for idleness before it. None is no time limit (0300): it runs while
169
+ // it works and pauses when idle.
170
+ ...(params.timeoutMs === undefined
171
+ ? {}
172
+ : { timeoutSeconds: Math.ceil(params.timeoutMs / 1000), idlePauseSeconds: 0 }),
169
173
  onLeaseEnd: "stop",
170
174
  network: {
171
175
  internet: !params.blockNetwork,
@@ -25,7 +25,7 @@ export const NETWORK_HELP = {
25
25
  ls Your domains
26
26
  get <hostname> One domain and its DNS records
27
27
  rm <hostname> Stop serving it
28
- Paid accounts only. The first visit gets the certificate, in a few seconds.
28
+ Needs a kept top-up. The first visit gets the certificate, in a few seconds.
29
29
  `,
30
30
  port: `runtime port <command>
31
31
 
@@ -34,7 +34,7 @@ export const NETWORK_HELP = {
34
34
  Prints address:port for your client
35
35
  ls [--sandbox <id>] Your open ports
36
36
  close <portId> Close one, its open connections too
37
- Paid accounts only. TCP, carried as it is: use your service's own password and TLS.
37
+ Needs a kept top-up. TCP, carried as it is: use your service's own password and TLS.
38
38
  `,
39
39
  address: `runtime address <command>
40
40
 
@@ -42,7 +42,7 @@ export const NETWORK_HELP = {
42
42
  account sends from it, for allow-lists
43
43
  ls Your addresses
44
44
  release <addressId> Give it back; sandboxes use the shared addresses again
45
- Paid accounts only.
45
+ Needs credit on the account.
46
46
  `,
47
47
  tunnel: `runtime tunnel <command>
48
48
 
@@ -56,7 +56,7 @@ export const NETWORK_HELP = {
56
56
  peer rotate <peerId> [--out runtime.conf]
57
57
  A new key; the old one stops working within seconds
58
58
  peer rm <peerId> Remove it; its tunnel ends within seconds
59
- --route: your own ranges behind the peer that sandboxes may reach. Paid accounts only.
59
+ --route: your own ranges behind the peer that sandboxes may reach. Needs a kept top-up.
60
60
  `,
61
61
  network: `runtime network <command>
62
62
 
@@ -69,13 +69,13 @@ export const NETWORK_HELP = {
69
69
  upstream-proxy get The proxy in use
70
70
  upstream-proxy remove Connections leave directly again
71
71
  A proxy that refuses or cannot be reached fails the connection; nothing goes
72
- around it. Paid accounts only.
72
+ around it. Needs a kept top-up.
73
73
 
74
74
  private on Your sandboxes reach each other by name, over TCP:
75
75
  <name>.sandbox.internal:<port> or <id>.sandbox.internal:<port>
76
76
  private off They stop; open connections between them are cut
77
77
  private status On or off
78
- Only your own sandboxes answer. Paid accounts only, and free.
78
+ Only your own sandboxes answer. Needs a kept top-up; free.
79
79
  `,
80
80
  };
81
81
  function usage(product, message) {
@@ -411,7 +411,7 @@ export async function networkProductCommand(product, argv, args, out, client) {
411
411
  ? "On: your sandboxes reach each other at <name>.sandbox.internal:<port> over TCP."
412
412
  : state.allowed
413
413
  ? "Off. Turn it on: runtime network private on"
414
- : "Off. Reaching your sandboxes by name is for paid accounts; add credit to turn it on.";
414
+ : "Off. Reaching your sandboxes by name needs a kept top-up.";
415
415
  switch (rest[0]) {
416
416
  case "on":
417
417
  case "off": {
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * const agent = new SandboxAgent({ name: "Coder", instructions: "Fix the failing test." });
13
13
  * await run(agent, "Go.", {
14
- * sandbox: { client: new RuntimeCloudSandboxClient({ create: { funding: "trial" } }) },
14
+ * sandbox: { client: new RuntimeCloudSandboxClient() },
15
15
  * });
16
16
  *
17
17
  * Needs `@openai/agents` 0.18 or newer beside this package. The key comes from
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * const agent = new SandboxAgent({ name: "Coder", instructions: "Fix the failing test." });
13
13
  * await run(agent, "Go.", {
14
- * sandbox: { client: new RuntimeCloudSandboxClient({ create: { funding: "trial" } }) },
14
+ * sandbox: { client: new RuntimeCloudSandboxClient() },
15
15
  * });
16
16
  *
17
17
  * Needs `@openai/agents` 0.18 or newer beside this package. The key comes from
@@ -34,7 +34,8 @@ export type BrowserStartOptions = {
34
34
  export declare function sandboxBrowser(t: Transport, sandbox: Sandbox): {
35
35
  /** Starts Chromium and returns its CDP address. The first start in a
36
36
  * sandbox installs Chromium, about a minute; this waits for it (up to 10
37
- * minutes). Starting a running browser returns it as it is. */
37
+ * minutes, or the call's timeoutMs). Starting a running browser returns it
38
+ * as it is. */
38
39
  start(input?: BrowserStartOptions, options?: RequestOptions): Promise<{
39
40
  running: true;
40
41
  headless: boolean;
@@ -1,3 +1,4 @@
1
+ import { whileInstalling } from "../wait.js";
1
2
  /** `sbx.browser`: Chromium in the sandbox, for agents that speak CDP.
2
3
  *
3
4
  * const { cdpUrl } = await sbx.browser.start();
@@ -9,26 +10,15 @@ export function sandboxBrowser(t, sandbox) {
9
10
  return {
10
11
  /** Starts Chromium and returns its CDP address. The first start in a
11
12
  * sandbox installs Chromium, about a minute; this waits for it (up to 10
12
- * minutes). Starting a running browser returns it as it is. */
13
+ * minutes, or the call's timeoutMs). Starting a running browser returns it
14
+ * as it is. */
13
15
  async start(input = {}, options) {
14
- const deadline = Date.now() + 600_000;
15
- for (;;) {
16
- try {
17
- return await t.json({
18
- method: "POST",
19
- path: `${base()}:start`,
20
- body: input,
21
- ...options,
22
- });
23
- }
24
- catch (error) {
25
- const code = error.code;
26
- if ((code !== "browser_installing" && code !== "desktop_installing") ||
27
- Date.now() > deadline)
28
- throw error;
29
- await new Promise((resolve) => setTimeout(resolve, error.retryAfterMs ?? 10_000));
30
- }
31
- }
16
+ return whileInstalling(["browser_installing", "desktop_installing"], options, (request) => t.json({
17
+ method: "POST",
18
+ path: `${base()}:start`,
19
+ body: input,
20
+ ...request,
21
+ }));
32
22
  },
33
23
  /** Whether it runs, and a fresh cdpUrl when it does. */
34
24
  get: (options) => t.json({ method: "GET", path: base(), ...options }),
@@ -53,7 +53,8 @@ export type RecordOptions = {
53
53
  * Coordinates are pixels from the top left of the screen. */
54
54
  export declare function sandboxDesktop(t: Transport, sandbox: Sandbox): {
55
55
  /** Starts the desktop. The first start in a sandbox installs it, which
56
- * takes a minute or two; this waits for that (up to 10 minutes). */
56
+ * takes a minute or two; this waits for that (up to 10 minutes, or the
57
+ * call's timeoutMs). */
57
58
  start(size?: {
58
59
  width?: number;
59
60
  height?: number;