withruntime 0.11.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 (60) hide show
  1. package/BLAXEL.md +7 -8
  2. package/DAYTONA.md +7 -8
  3. package/E2B.md +16 -2
  4. package/README.md +3 -3
  5. package/README.src.md +3 -3
  6. package/VERCEL.md +3 -2
  7. package/compatibility-lock.json +55 -0
  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 +39 -15
  15. package/dist/client.d.ts +5 -1
  16. package/dist/client.js +19 -3
  17. package/dist/compat/files.js +5 -1
  18. package/dist/compat/lease.d.ts +1 -0
  19. package/dist/compat/lease.js +14 -8
  20. package/dist/credentials.d.ts +9 -0
  21. package/dist/credentials.js +26 -4
  22. package/dist/daytona/daytona.js +29 -12
  23. package/dist/daytona/sandbox.d.ts +8 -3
  24. package/dist/daytona/sandbox.js +23 -4
  25. package/dist/e2b/sandbox.d.ts +7 -3
  26. package/dist/e2b/sandbox.js +21 -7
  27. package/dist/errors.d.ts +6 -0
  28. package/dist/errors.js +6 -0
  29. package/dist/identity.js +2 -1
  30. package/dist/index.d.ts +2 -1
  31. package/dist/index.js +2 -1
  32. package/dist/modal/index.js +6 -2
  33. package/dist/network-cli.js +7 -7
  34. package/dist/products/browser.d.ts +2 -1
  35. package/dist/products/browser.js +9 -19
  36. package/dist/products/desktop.d.ts +2 -1
  37. package/dist/products/desktop.js +5 -30
  38. package/dist/products/jobs.d.ts +7 -5
  39. package/dist/products/jobs.js +7 -3
  40. package/dist/products/mcp.d.ts +5 -1
  41. package/dist/products/mcp.js +29 -8
  42. package/dist/products/network-products.d.ts +1 -1
  43. package/dist/products/network-products.js +1 -1
  44. package/dist/products/observability.d.ts +1 -1
  45. package/dist/products/private-network.d.ts +1 -1
  46. package/dist/products/secrets.d.ts +2 -2
  47. package/dist/runloop/index.js +6 -2
  48. package/dist/sandbox.d.ts +27 -19
  49. package/dist/sandbox.js +121 -54
  50. package/dist/tar.d.ts +2 -0
  51. package/dist/tar.js +74 -16
  52. package/dist/transport.d.ts +33 -7
  53. package/dist/transport.js +171 -36
  54. package/dist/types.d.ts +54 -9
  55. package/dist/vercel/filesystem.js +3 -1
  56. package/dist/vercel/sandbox.d.ts +17 -10
  57. package/dist/vercel/sandbox.js +26 -13
  58. package/dist/wait.d.ts +12 -0
  59. package/dist/wait.js +47 -0
  60. package/package.json +1 -1
@@ -1,9 +1,11 @@
1
1
  import { RuntimeError } from "../errors.js";
2
- /* A rival's sandbox may be asked to live longer than one Runtime lease (an
3
- hour at most, ARCHITECTURE.md section 10): Vercel's `timeout` over an hour,
4
- Daytona's `autoStopInterval` over an hour or 0. The drop-ins keep the lease
5
- reaching toward the time asked for while their sandbox object lives, one
6
- extension at a time, never past that time. */
2
+ /* A rival's sandbox may be asked to live longer than one Runtime time limit
3
+ (an hour at most, ARCHITECTURE.md section 10): Vercel's `timeout` over an
4
+ hour, Daytona's `autoStopInterval` over an hour. The drop-ins keep the
5
+ limit reaching toward the time asked for while their sandbox object lives,
6
+ one extension at a time, never past that time. A sandbox created with no
7
+ time limit renews itself on the server (0300; `endsAt` null) and is left
8
+ alone. */
7
9
  /** The most one lease runs ahead of now. */
8
10
  export const LEASE_MAX_SECONDS = 3600;
9
11
  /** Whole seconds to add to a lease ending at `expiresAt` (epoch ms) so it
@@ -39,7 +41,10 @@ export class LeaseKeeper {
39
41
  #schedule() {
40
42
  if (this.#ended || this.#timer)
41
43
  return;
42
- const expiresAt = Date.parse(this.#sandbox().info.expiresAt);
44
+ const info = this.#sandbox().info;
45
+ if (info.endsAt === null)
46
+ return;
47
+ const expiresAt = Date.parse(info.endsAt ?? info.expiresAt);
43
48
  if (!Number.isFinite(expiresAt) || this.#until() <= expiresAt)
44
49
  return;
45
50
  this.#timer = setTimeout(() => {
@@ -61,9 +66,10 @@ export class LeaseKeeper {
61
66
  }
62
67
  async #extend(now) {
63
68
  const sandbox = this.#sandbox();
64
- if (sandbox.state !== "running")
69
+ // No time limit: the server renews it, and an extension would change nothing.
70
+ if (sandbox.state !== "running" || sandbox.info.endsAt === null)
65
71
  return;
66
- const seconds = extensionSeconds(Date.parse(sandbox.info.expiresAt), this.#until(), Date.now(), now ? Infinity : this.#marginMs());
72
+ const seconds = extensionSeconds(Date.parse(sandbox.info.endsAt ?? sandbox.info.expiresAt), this.#until(), Date.now(), now ? Infinity : this.#marginMs());
67
73
  if (seconds < 1)
68
74
  return;
69
75
  try {
@@ -10,6 +10,15 @@ export type SavedConnection = {
10
10
  /** The account's name when it was saved, for `runtime account`. */
11
11
  orgName?: string;
12
12
  };
13
+ /** Saved accounts could not be updated because another command holds their
14
+ * lock: still `busy` after the whole wait, or `interrupted`, left by a command
15
+ * that died holding it. The message names the lock file to remove. */
16
+ export declare class CredentialsLockedError extends Error {
17
+ readonly lockFile: string;
18
+ readonly reason: "busy" | "interrupted";
19
+ readonly name = "CredentialsLockedError";
20
+ constructor(message: string, lockFile: string, reason: "busy" | "interrupted");
21
+ }
13
22
  export declare function connectionOrigins(env: NodeJS.ProcessEnv): {
14
23
  api: string;
15
24
  auth: string;
@@ -4,6 +4,23 @@ import { lstat, mkdir, open, rename, unlink } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
5
  import { isAbsolute, join } from "node:path";
6
6
  import { apiOrigin, missingKey } from "./transport.js";
7
+ /** Saved accounts could not be updated because another command holds their
8
+ * lock: still `busy` after the whole wait, or `interrupted`, left by a command
9
+ * that died holding it. The message names the lock file to remove. */
10
+ export class CredentialsLockedError extends Error {
11
+ lockFile;
12
+ reason;
13
+ name = "CredentialsLockedError";
14
+ constructor(message, lockFile, reason) {
15
+ super(message);
16
+ this.lockFile = lockFile;
17
+ this.reason = reason;
18
+ }
19
+ }
20
+ /** How long a save waits for another command's update to finish. An update
21
+ * is one read, modify and write of small files, so only a heavily loaded
22
+ * machine comes near it. */
23
+ const LOCK_WAIT_MS = 30_000;
7
24
  /** How many other accounts' connections one machine keeps beside the one in use. */
8
25
  const SAVED_ACCOUNTS = 20;
9
26
  export function connectionOrigins(env) {
@@ -107,7 +124,7 @@ export function connectionStore(env, directory = defaultDirectory(env)) {
107
124
  * Never steal a lock: stale cleanup can otherwise delete a new live lock. */
108
125
  async function updateAccounts(update) {
109
126
  await privateDirectory(true);
110
- const until = performance.now() + 10_000;
127
+ const until = performance.now() + LOCK_WAIT_MS;
111
128
  let handle;
112
129
  for (;;) {
113
130
  try {
@@ -127,16 +144,21 @@ export function connectionStore(env, directory = defaultDirectory(env)) {
127
144
  // The holder may not have finished writing its process id yet.
128
145
  }
129
146
  if (typeof pid === "number" && Number.isSafeInteger(pid) && pid > 0) {
147
+ let gone = false;
130
148
  try {
131
149
  process.kill(pid, 0);
132
150
  }
133
151
  catch (error) {
134
- if (error.code === "ESRCH")
135
- throw new Error(`A previous Runtime credential update was interrupted. Remove ${lockFile} and retry.`);
152
+ gone = error.code === "ESRCH";
136
153
  }
154
+ // A holder that finished between our read and our check has exited
155
+ // too, and its lock is gone or another's: wait for the lock again.
156
+ // Only a lock still naming the dead process was left by one that died.
157
+ if (gone && (await readPrivate(lockFile, 1024, "credential update lock")) === text)
158
+ throw new CredentialsLockedError(`A previous Runtime credential update was interrupted. Remove ${lockFile} and retry.`, lockFile, "interrupted");
137
159
  }
138
160
  if (performance.now() >= until)
139
- throw new Error(`Another Runtime command is updating saved accounts. Retry when it finishes. If no command is running, remove ${lockFile} and retry.`);
161
+ throw new CredentialsLockedError(`Another Runtime command is updating saved accounts. Retry when it finishes. If no command is running, remove ${lockFile} and retry.`, lockFile, "busy");
140
162
  await new Promise((resolve) => setTimeout(resolve, 25));
141
163
  }
142
164
  try {
@@ -1,6 +1,7 @@
1
1
  import { clientFor, } from "./client.js";
2
2
  import { DaytonaError, DaytonaNotFoundError, guard, NotSupportedError } from "./errors.js";
3
- import { idleSeconds, networkRules, notFound, retentionDays, Sandbox, windowSeconds, } from "./sandbox.js";
3
+ import { LEASE_MAX_SECONDS } from "../compat/lease.js";
4
+ import { idlePauseOf, idleSeconds, networkRules, notFound, retentionDays, Sandbox, windowSeconds, } from "./sandbox.js";
4
5
  /* Daytona's client over Runtime's. `new Daytona()` then `daytona.create()`
5
6
  makes a Runtime sandbox with Daytona's defaults. */
6
7
  /** Daytona's default machine: 1 vCPU, 1 GiB memory, 3 GiB disk (checked
@@ -211,9 +212,15 @@ export class Daytona {
211
212
  memoryMiB: Math.round(resources.memory * 1024),
212
213
  diskMiB: Math.round(resources.disk * 1024),
213
214
  }),
214
- timeoutSeconds: lifecycle.deadline
215
- ? Math.max(60, Math.min(lifecycle.windowSeconds, Math.floor((lifecycle.deadline - Date.now()) / 1000)))
216
- : lifecycle.windowSeconds,
215
+ // A time to live is a time limit, kept by the lease. Without one there
216
+ // is none: it runs while it works and autoStopInterval is its idle
217
+ // pause, counted by the sandbox itself, so a long command is never
218
+ // frozen for want of calls from this client (0300).
219
+ ...(lifecycle.deadline
220
+ ? {
221
+ timeoutSeconds: Math.max(60, Math.min(lifecycle.windowSeconds, Math.floor((lifecycle.deadline - Date.now()) / 1000))),
222
+ }
223
+ : { idlePauseSeconds: idlePauseOf(lifecycle.autoStopInterval) }),
217
224
  onLeaseEnd: lifecycle.ephemeral || lifecycle.deadline ? "stop" : "pause",
218
225
  ...(params.name ? { name: params.name } : {}),
219
226
  ...(labels ? { labels } : {}),
@@ -303,14 +310,24 @@ export class Daytona {
303
310
  language: runtime.info.labels[LANGUAGE_LABEL] ?? "python",
304
311
  public: false,
305
312
  ...(user ? { user } : {}),
306
- lifecycle: {
307
- windowSeconds: runtime.info.timeoutSeconds,
308
- idleSeconds: runtime.info.timeoutSeconds,
309
- ephemeral: onLeaseEnd === "stop",
310
- autoStopInterval: Math.round(runtime.info.timeoutSeconds / 60),
311
- autoArchiveInterval: 0,
312
- autoDeleteInterval: -1,
313
- },
313
+ lifecycle: runtime.info.timeoutSeconds
314
+ ? {
315
+ windowSeconds: runtime.info.timeoutSeconds,
316
+ idleSeconds: runtime.info.timeoutSeconds,
317
+ ephemeral: onLeaseEnd === "stop",
318
+ autoStopInterval: Math.round(runtime.info.timeoutSeconds / 60),
319
+ autoArchiveInterval: 0,
320
+ autoDeleteInterval: -1,
321
+ }
322
+ : {
323
+ // No time limit: its idle pause is its autoStopInterval.
324
+ windowSeconds: LEASE_MAX_SECONDS,
325
+ idleSeconds: Infinity,
326
+ ephemeral: onLeaseEnd === "stop",
327
+ autoStopInterval: Math.round((runtime.info.idlePauseSeconds ?? 0) / 60),
328
+ autoArchiveInterval: 0,
329
+ autoDeleteInterval: -1,
330
+ },
314
331
  });
315
332
  }
316
333
  /** Live and stopped (paused) sandboxes, oldest first. Await it for the old
@@ -134,8 +134,9 @@ export declare class Sandbox {
134
134
  refreshData(): Promise<void>;
135
135
  /** Counts as activity: moves the lease a full autoStop window on. */
136
136
  refreshActivity(): Promise<void>;
137
- /** The sandbox pauses after `interval` minutes without a call from this
138
- * client; 0 never. Past an hour, while this object lives. */
137
+ /** The sandbox pauses after `interval` minutes with nothing happening in
138
+ * it; 0 never. With a time to live, after that long without a call from
139
+ * this client, and past an hour only while this object lives. */
139
140
  setAutostopInterval(interval: number): Promise<void>;
140
141
  setAutoPauseInterval(interval: number): Promise<void>;
141
142
  /** Runtime has no archive tier; recorded only. */
@@ -192,8 +193,12 @@ export declare class Sandbox {
192
193
  }
193
194
  /** Daytona's autoStopInterval (minutes; 0 is off) as the lease window. */
194
195
  export declare function windowSeconds(autoStopMinutes: number): number;
196
+ /** Daytona's autoStopInterval (minutes; 0 is never) as the idle pause of a
197
+ * sandbox with no time limit: seconds with nothing happening in it, counted
198
+ * by the sandbox itself (`idlePauseSeconds`, 0 never, at most a day). */
199
+ export declare function idlePauseOf(autoStopMinutes: number): number;
195
200
  /** Daytona's autoStopInterval (minutes; 0 is never) as seconds without a
196
- * call before the sandbox pauses. */
201
+ * call before a sandbox with a time to live pauses. */
197
202
  export declare function idleSeconds(autoStopMinutes: number): number;
198
203
  /** Minutes as Runtime's retention: whole days, 1 to 365. */
199
204
  export declare function retentionDays(minutes: number): number;
@@ -238,7 +238,11 @@ export class Sandbox {
238
238
  if (state === "stopped" || state === "stopping")
239
239
  throw new DaytonaError(`Sandbox ${this.id} was deleted and cannot start.`, 410);
240
240
  if (state === "paused" || state === "pausing")
241
- await guard("sandbox", () => this.#rt.wake({ timeoutSeconds: this.#options.lifecycle.windowSeconds }));
241
+ await guard("sandbox", () =>
242
+ // One with no time limit (timeoutSeconds 0) wakes with none.
243
+ this.#rt.wake(this.#rt.info.timeoutSeconds
244
+ ? { timeoutSeconds: this.#options.lifecycle.windowSeconds }
245
+ : {}));
242
246
  this.#pausedByPause = false;
243
247
  }
244
248
  /** Stops the sandbox: Runtime pauses it, keeping its files and memory, so
@@ -291,11 +295,16 @@ export class Sandbox {
291
295
  async refreshActivity() {
292
296
  await guard("sandbox", () => this.#live(true));
293
297
  }
294
- /** The sandbox pauses after `interval` minutes without a call from this
295
- * client; 0 never. Past an hour, while this object lives. */
298
+ /** The sandbox pauses after `interval` minutes with nothing happening in
299
+ * it; 0 never. With a time to live, after that long without a call from
300
+ * this client, and past an hour only while this object lives. */
296
301
  async setAutostopInterval(interval) {
297
302
  const lifecycle = this.#options.lifecycle;
298
303
  lifecycle.autoStopInterval = interval;
304
+ if (!this.#rt.info.timeoutSeconds) {
305
+ await guard("sandbox", () => this.#rt.update({ idlePauseSeconds: idlePauseOf(interval) }));
306
+ return;
307
+ }
299
308
  lifecycle.windowSeconds = windowSeconds(interval);
300
309
  lifecycle.idleSeconds = idleSeconds(interval);
301
310
  await guard("sandbox", () => this.#live());
@@ -321,6 +330,8 @@ export class Sandbox {
321
330
  delete this.#options.lifecycle.deadline;
322
331
  return;
323
332
  }
333
+ if (!this.#rt.info.timeoutSeconds)
334
+ throw new NotSupportedError("A time to live on a sandbox made without one", "It has no time limit and pauses when idle; pass ttlMinutes to create(), or call stop() or delete() when the work is done.");
324
335
  const deadline = Date.parse(this.#rt.info.createdAt) + ttlMinutes * 60_000;
325
336
  if (Date.parse(this.#rt.info.expiresAt) > deadline)
326
337
  throw new NotSupportedError("A time to live shorter than the current lease", "Runtime leases only move later; call stop() or delete() when the work is done.");
@@ -444,8 +455,16 @@ export function windowSeconds(autoStopMinutes) {
444
455
  return 3600;
445
456
  return Math.min(3600, Math.max(60, Math.round(autoStopMinutes * 60)));
446
457
  }
458
+ /** Daytona's autoStopInterval (minutes; 0 is never) as the idle pause of a
459
+ * sandbox with no time limit: seconds with nothing happening in it, counted
460
+ * by the sandbox itself (`idlePauseSeconds`, 0 never, at most a day). */
461
+ export function idlePauseOf(autoStopMinutes) {
462
+ if (autoStopMinutes <= 0)
463
+ return 0;
464
+ return Math.min(86_400, Math.max(60, Math.round(autoStopMinutes * 60)));
465
+ }
447
466
  /** Daytona's autoStopInterval (minutes; 0 is never) as seconds without a
448
- * call before the sandbox pauses. */
467
+ * call before a sandbox with a time to live pauses. */
449
468
  export function idleSeconds(autoStopMinutes) {
450
469
  if (autoStopMinutes <= 0)
451
470
  return Infinity;
@@ -9,7 +9,9 @@ export declare const DEFAULT_SHAPE: {
9
9
  readonly vcpu: 2;
10
10
  readonly memoryMiB: 512;
11
11
  };
12
- /** E2B's default sandbox timeout, 300 s. */
12
+ /** E2B's default sandbox timeout, 300 s. Not sent: a sandbox created with no
13
+ * timeoutMs has no time limit on Runtime, running while it works and pausing
14
+ * when idle (0300). Kept for code that imports it. */
13
15
  export declare const DEFAULT_TIMEOUT_MS = 300000;
14
16
  export type SandboxState = "running" | "paused";
15
17
  export type SandboxOnTimeout = "pause" | "kill" | {
@@ -30,7 +32,8 @@ export interface SandboxOpts extends ConnectionOpts {
30
32
  /** The sandbox's own environment: every command, terminal and code run in it
31
33
  * gets these, whoever connects. Values are never shown again. */
32
34
  envs?: Record<string, string>;
33
- /** Default 300 000, at most 24 hours. Runtime's leases run 60 s to 1 hour:
35
+ /** When it ends, at most 24 hours. Leave it out for no time limit: it runs
36
+ * while it works and pauses when idle. Runtime's limits run 60 s to 1 hour:
34
37
  * shorter rounds up to 60 s (with a warning); longer gets an hour, moved on
35
38
  * while this process runs, up to the time asked for. */
36
39
  timeoutMs?: number;
@@ -125,7 +128,8 @@ export declare class Sandbox {
125
128
  kill(opts?: Pick<ConnectionOpts, "requestTimeoutMs" | "signal">): Promise<boolean>;
126
129
  /** Sets the sandbox to end `timeoutMs` from now, up to 24 hours; past an
127
130
  * hour the lease is moved on while this process runs. Runtime cannot end a
128
- * lease early, so an end sooner than the lease's is refused. */
131
+ * lease early, so an end sooner than the lease's is refused. A sandbox
132
+ * created with no timeout has no end to move, and nothing changes. */
129
133
  static setTimeout(sandboxId: string, timeoutMs: number, opts?: SandboxApiOpts): Promise<void>;
130
134
  setTimeout(timeoutMs: number, opts?: Pick<ConnectionOpts, "requestTimeoutMs" | "signal">): Promise<void>;
131
135
  static getInfo(sandboxId: string, opts?: SandboxApiOpts): Promise<SandboxInfo>;
@@ -9,7 +9,9 @@ import { Pty } from "./pty.js";
9
9
  /** E2B's default machine: 2 vCPU and 512 MiB (docs.e2b.dev/billing, checked
10
10
  * 23 September 2026). Pass `runtime: { create: { memoryMiB } }` for another. */
11
11
  export const DEFAULT_SHAPE = { vcpu: 2, memoryMiB: 512 };
12
- /** E2B's default sandbox timeout, 300 s. */
12
+ /** E2B's default sandbox timeout, 300 s. Not sent: a sandbox created with no
13
+ * timeoutMs has no time limit on Runtime, running while it works and pausing
14
+ * when idle (0300). Kept for code that imports it. */
13
15
  export const DEFAULT_TIMEOUT_MS = 300_000;
14
16
  /** Runtime's lease bounds (MAX_TIMEOUT_SECONDS in the API). */
15
17
  const MIN_LEASE_SECONDS = 60;
@@ -93,6 +95,8 @@ async function renew(keeper) {
93
95
  if (current === "stopped")
94
96
  return stopKeeping(id);
95
97
  if (current === "running") {
98
+ if (keeper.runtime.info.endsAt === null)
99
+ return stopKeeping(id);
96
100
  const end = Date.parse(keeper.runtime.info.expiresAt);
97
101
  const want = Math.min(keeper.until, Date.now() + KEEP_AHEAD_MS);
98
102
  const later = Math.floor((want - end) / 1000);
@@ -200,7 +204,8 @@ function infoOf(runtime) {
200
204
  ...(info.name ? { name: info.name } : {}),
201
205
  metadata: info.labels ?? {},
202
206
  startedAt: new Date(info.createdAt),
203
- endAt: new Date(info.expiresAt),
207
+ // With no time limit, where it is paid up to: always ahead, moving on.
208
+ endAt: new Date(info.endsAt ?? info.expiresAt),
204
209
  state: current === "stopped" ? "paused" : current,
205
210
  cpuCount: info.vcpu,
206
211
  memoryMB: info.memoryMiB,
@@ -301,9 +306,10 @@ export class Sandbox {
301
306
  options.signal?.throwIfAborted();
302
307
  refuseCreate(opts);
303
308
  const client = clientFor(opts);
304
- const asked = Date.now() + (opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
309
+ // No timeoutMs, no time limit: it runs while it works (0300).
310
+ const asked = opts.timeoutMs === undefined ? undefined : Date.now() + opts.timeoutMs;
305
311
  const input = {
306
- timeoutSeconds: leaseSeconds(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
312
+ ...(opts.timeoutMs === undefined ? {} : { timeoutSeconds: leaseSeconds(opts.timeoutMs) }),
307
313
  onLeaseEnd: onLeaseEnd(opts.lifecycle),
308
314
  // With a lifecycle, E2B resumes a paused sandbox on traffic only when
309
315
  // asked (autoResume); Runtime's automatic wake is the same thing (0093).
@@ -324,7 +330,8 @@ export class Sandbox {
324
330
  ...opts.runtime?.create,
325
331
  };
326
332
  const runtime = await guard("sandbox", () => client.sandboxes.create(create, options));
327
- keepUntil(runtime, asked);
333
+ if (asked !== undefined)
334
+ keepUntil(runtime, asked);
328
335
  return new this(runtime, client, opts.requestTimeoutMs);
329
336
  }
330
337
  /** Connects to a sandbox by id, waking it if it is paused, as E2B does. A
@@ -373,7 +380,8 @@ export class Sandbox {
373
380
  }
374
381
  /** Sets the sandbox to end `timeoutMs` from now, up to 24 hours; past an
375
382
  * hour the lease is moved on while this process runs. Runtime cannot end a
376
- * lease early, so an end sooner than the lease's is refused. */
383
+ * lease early, so an end sooner than the lease's is refused. A sandbox
384
+ * created with no timeout has no end to move, and nothing changes. */
377
385
  static async setTimeout(sandboxId, timeoutMs, opts = {}) {
378
386
  const client = clientFor(opts);
379
387
  const runtime = await guard("sandbox", () => client.sandboxes.get(sandboxId, request(opts)));
@@ -547,6 +555,9 @@ async function resume(runtime, opts) {
547
555
  }
548
556
  else if (opts.timeoutMs !== undefined) {
549
557
  checkTimeout(opts.timeoutMs);
558
+ // No time limit: no end to move (0300).
559
+ if (runtime.info.endsAt === null)
560
+ return;
550
561
  const later = Math.min(asked, Date.now() + KEEP_AHEAD_MS) - Date.parse(runtime.info.expiresAt);
551
562
  if (later > 1000)
552
563
  await guard("sandbox", () => runtime.extend(Math.ceil(later / 1000), request(opts)));
@@ -562,8 +573,11 @@ async function stop(runtime, opts) {
562
573
  }
563
574
  async function extendTo(runtime, timeoutMs, opts) {
564
575
  checkTimeout(timeoutMs);
576
+ // No time limit: no end to move (0300).
577
+ if (runtime.info.endsAt === null)
578
+ return;
565
579
  const asked = Date.now() + timeoutMs;
566
- const end = Date.parse(runtime.info.expiresAt);
580
+ const end = Date.parse(runtime.info.endsAt ?? runtime.info.expiresAt);
567
581
  if (asked - end < -1000)
568
582
  throw new NotSupportedError("Ending a sandbox sooner than its lease (a shorter setTimeout)", "Runtime cannot end a lease early. Call kill() when the work is done.");
569
583
  const later = Math.min(asked, Date.now() + KEEP_AHEAD_MS) - end;
package/dist/errors.d.ts CHANGED
@@ -6,6 +6,12 @@
6
6
  * packages/cloud/src/api/respond.ts) and retrying cannot change them.
7
7
  * host_unavailable and busy pass, and are retried. */
8
8
  export declare const DELIBERATE: ReadonlySet<string>;
9
+ /** Refusals that guarantee nothing ran: a sandbox running as many commands
10
+ * as it can (`guest_busy`), and the API's own limits, which answer before any
11
+ * work starts (`busy`, `rate_limited`). Sending such a call again is always
12
+ * safe, so the SDK does, with backoff, until the call's deadline (five
13
+ * minutes when it has none), however many retries it allows otherwise. */
14
+ export declare const NOTHING_RAN: ReadonlySet<string>;
9
15
  /** Refusals of a create that clear when a sandbox stops or pauses, or a host
10
16
  * frees room: the trial's slots, an email domain's trial slots, the account's
11
17
  * quota and the region's capacity. `sandboxes.create` waits them out, retrying
package/dist/errors.js CHANGED
@@ -16,6 +16,12 @@ export const DELIBERATE = new Set([
16
16
  "identity_unavailable",
17
17
  "env_unavailable",
18
18
  ]);
19
+ /** Refusals that guarantee nothing ran: a sandbox running as many commands
20
+ * as it can (`guest_busy`), and the API's own limits, which answer before any
21
+ * work starts (`busy`, `rate_limited`). Sending such a call again is always
22
+ * safe, so the SDK does, with backoff, until the call's deadline (five
23
+ * minutes when it has none), however many retries it allows otherwise. */
24
+ export const NOTHING_RAN = new Set(["guest_busy", "busy", "rate_limited"]);
19
25
  /** Refusals that pass on their own: a host frees room, a trial slot frees. */
20
26
  const PASSING = new Set(["no_capacity", "trial_busy"]);
21
27
  /** Refusals of a create that clear when a sandbox stops or pauses, or a host
package/dist/identity.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { RuntimeError } from "./errors.js";
2
+ import { sandboxMarker } from "./transport.js";
2
3
  async function requestCredentials() {
3
4
  let url = process.env.RUNTIME_ID_TOKEN_REQUEST_URL;
4
5
  let token = process.env.RUNTIME_ID_TOKEN_REQUEST_TOKEN;
@@ -7,7 +8,7 @@ async function requestCredentials() {
7
8
  try {
8
9
  // Loaded here, so the SDK still loads where there is no file system.
9
10
  const { readFile } = await import("node:fs/promises");
10
- const env = JSON.parse(await readFile("/run/runtime/environment.json", "utf8"));
11
+ const env = JSON.parse(await readFile(sandboxMarker.path, "utf8"));
11
12
  url = env.RUNTIME_ID_TOKEN_REQUEST_URL;
12
13
  token = env.RUNTIME_ID_TOKEN_REQUEST_TOKEN;
13
14
  }
package/dist/index.d.ts CHANGED
@@ -35,7 +35,8 @@ export type * from "./types.js";
35
35
  * does the same with an explicit client. */
36
36
  export declare class Sandbox extends SandboxClass {
37
37
  /** Creates a sandbox (every field optional) and waits until it is running.
38
- * `keepAlive: true` (or its options) extends its lease in the background
38
+ * With no `timeoutSeconds` it runs while it works and pauses when idle.
39
+ * `keepAlive: true` (or its options) carries one with a time limit past it
39
40
  * until you stop it; see `sandbox.keepAlive()`. */
40
41
  static create(input?: CreateSandbox & {
41
42
  wait?: boolean;
package/dist/index.js CHANGED
@@ -14,7 +14,8 @@ export * from "./errors.js";
14
14
  * does the same with an explicit client. */
15
15
  export class Sandbox extends SandboxClass {
16
16
  /** Creates a sandbox (every field optional) and waits until it is running.
17
- * `keepAlive: true` (or its options) extends its lease in the background
17
+ * With no `timeoutSeconds` it runs while it works and pauses when idle.
18
+ * `keepAlive: true` (or its options) carries one with a time limit past it
18
19
  * until you stop it; see `sandbox.keepAlive()`. */
19
20
  static async create(input = {}, options = {}) {
20
21
  const { client, ...rest } = options;
@@ -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": {
@@ -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;