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
@@ -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 {
@@ -177,8 +177,7 @@ export declare class Daytona implements AsyncDisposable {
177
177
  [Symbol.asyncDispose](): Promise<void>;
178
178
  /** Creates a sandbox and waits until it runs, with Daytona's defaults: 1
179
179
  * vCPU, 1 GiB, 3 GiB disk, pausing after 15 minutes without calls. Funding
180
- * is left to Runtime: the free trial while the account has trial time, then
181
- * prepaid credit, exactly as withruntime's own create. */
180
+ * is left to Runtime: the included usage first, then prepaid credit, exactly as withruntime's own create. */
182
181
  create(params?: CreateSandboxFromSnapshotParams | CreateSandboxFromImageParams, options?: {
183
182
  timeout?: number;
184
183
  onSnapshotCreateLogs?: (chunk: string) => void;
@@ -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
@@ -182,8 +183,7 @@ export class Daytona {
182
183
  async [Symbol.asyncDispose]() { }
183
184
  /** Creates a sandbox and waits until it runs, with Daytona's defaults: 1
184
185
  * vCPU, 1 GiB, 3 GiB disk, pausing after 15 minutes without calls. Funding
185
- * is left to Runtime: the free trial while the account has trial time, then
186
- * prepaid credit, exactly as withruntime's own create. */
186
+ * is left to Runtime: the included usage first, then prepaid credit, exactly as withruntime's own create. */
187
187
  async create(params = {}, options = {}) {
188
188
  refuseCreate(params);
189
189
  const client = params.withruntime?.client ?? this.#client;
@@ -211,9 +211,15 @@ export class Daytona {
211
211
  memoryMiB: Math.round(resources.memory * 1024),
212
212
  diskMiB: Math.round(resources.disk * 1024),
213
213
  }),
214
- timeoutSeconds: lifecycle.deadline
215
- ? Math.max(60, Math.min(lifecycle.windowSeconds, Math.floor((lifecycle.deadline - Date.now()) / 1000)))
216
- : lifecycle.windowSeconds,
214
+ // A time to live is a time limit, kept by the lease. Without one there
215
+ // is none: it runs while it works and autoStopInterval is its idle
216
+ // pause, counted by the sandbox itself, so a long command is never
217
+ // frozen for want of calls from this client (0300).
218
+ ...(lifecycle.deadline
219
+ ? {
220
+ timeoutSeconds: Math.max(60, Math.min(lifecycle.windowSeconds, Math.floor((lifecycle.deadline - Date.now()) / 1000))),
221
+ }
222
+ : { idlePauseSeconds: idlePauseOf(lifecycle.autoStopInterval) }),
217
223
  onLeaseEnd: lifecycle.ephemeral || lifecycle.deadline ? "stop" : "pause",
218
224
  ...(params.name ? { name: params.name } : {}),
219
225
  ...(labels ? { labels } : {}),
@@ -303,14 +309,24 @@ export class Daytona {
303
309
  language: runtime.info.labels[LANGUAGE_LABEL] ?? "python",
304
310
  public: false,
305
311
  ...(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
- },
312
+ lifecycle: runtime.info.timeoutSeconds
313
+ ? {
314
+ windowSeconds: runtime.info.timeoutSeconds,
315
+ idleSeconds: runtime.info.timeoutSeconds,
316
+ ephemeral: onLeaseEnd === "stop",
317
+ autoStopInterval: Math.round(runtime.info.timeoutSeconds / 60),
318
+ autoArchiveInterval: 0,
319
+ autoDeleteInterval: -1,
320
+ }
321
+ : {
322
+ // No time limit: its idle pause is its autoStopInterval.
323
+ windowSeconds: LEASE_MAX_SECONDS,
324
+ idleSeconds: Infinity,
325
+ ephemeral: onLeaseEnd === "stop",
326
+ autoStopInterval: Math.round((runtime.info.idlePauseSeconds ?? 0) / 60),
327
+ autoArchiveInterval: 0,
328
+ autoDeleteInterval: -1,
329
+ },
314
330
  });
315
331
  }
316
332
  /** Live and stopped (paused) sandboxes, oldest first. Await it for the old
@@ -125,7 +125,7 @@ export declare class Sandbox {
125
125
  /** Pauses the sandbox with its memory. */
126
126
  pause(_timeout?: number): Promise<void>;
127
127
  /** Runtime has no archive tier: a stopped sandbox stays paused, kept for
128
- * its retention (30 days paid, 7 on the trial, or autoDeleteInterval). */
128
+ * its retention (30 days paid, 7 without credit, or autoDeleteInterval). */
129
129
  archive(): Promise<void>;
130
130
  /** Ends the sandbox for good. */
131
131
  delete(_timeout?: number): Promise<void>;
@@ -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
@@ -265,7 +269,7 @@ export class Sandbox {
265
269
  this.#pausedByPause = true;
266
270
  }
267
271
  /** Runtime has no archive tier: a stopped sandbox stays paused, kept for
268
- * its retention (30 days paid, 7 on the trial, or autoDeleteInterval). */
272
+ * its retention (30 days paid, 7 without credit, or autoDeleteInterval). */
269
273
  async archive() {
270
274
  await this.stop();
271
275
  }
@@ -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;
@@ -43,15 +43,28 @@ export interface RuntimeOpts {
43
43
  /** Refuses the E2B-only connection options: they point at E2B, and quietly
44
44
  * dropping one would send the call somewhere its author did not mean. */
45
45
  export declare function refuseE2BConnection(opts?: ConnectionOpts): void;
46
+ /** Connections the adapter's own client may hold at once: E2B's SDK has no
47
+ * cap of its own, so an orchestrator that drives a thousand sandboxes from one
48
+ * process runs every command at once. 4,096 is the widest room Runtime's edge
49
+ * gives an address that has used a valid key (api.md, "Limits"); the edge and
50
+ * the API answer anything past an account's room with a retry, which the
51
+ * client waits out. Runtime's own SDK keeps 48. */
52
+ export declare const E2B_MAX_CONNECTIONS = 4096;
53
+ /** Said once per process, the first time a call has to wait for a
54
+ * connection: E2B never holds a call back, so a caller should hear it. */
55
+ export declare function warnQueued(maxConnections: number): void;
46
56
  /** The Runtime client for these options: an explicit client, else one per key,
47
57
  * made once and reused so its connections stay open. */
48
58
  export declare function clientFor(opts?: ConnectionOpts & {
49
59
  runtime?: RuntimeOpts;
50
60
  }): Runtime;
51
61
  /** Explicit key, then RUNTIME_API_KEY, then E2B_API_KEY when it holds a
52
- * Runtime key; undefined lets the Runtime SDK find the saved login. */
62
+ * Runtime key; undefined, when none is set, lets the Runtime SDK find the
63
+ * saved login. An E2B key is refused, never sent. */
53
64
  export declare function pickKey(explicit?: string): string | undefined;
54
- /** Test hook: forget the cached clients. */
65
+ /** Where the key in use came from, as an error names it. */
66
+ export declare function keySource(explicit?: string): string;
67
+ /** Test hook: forget the cached clients and the warnings. */
55
68
  export declare function resetClients(): void;
56
69
  /** Request options for a Runtime call from E2B's. */
57
70
  export declare function request(opts?: {
@@ -26,6 +26,23 @@ export function refuseE2BConnection(opts = {}) {
26
26
  throw new NotSupportedError("E2B's debug mode (a local envd)", "Remove debug: true; there is no local Runtime guest to connect to.");
27
27
  }
28
28
  const clients = new Map();
29
+ /** Connections the adapter's own client may hold at once: E2B's SDK has no
30
+ * cap of its own, so an orchestrator that drives a thousand sandboxes from one
31
+ * process runs every command at once. 4,096 is the widest room Runtime's edge
32
+ * gives an address that has used a valid key (api.md, "Limits"); the edge and
33
+ * the API answer anything past an account's room with a retry, which the
34
+ * client waits out. Runtime's own SDK keeps 48. */
35
+ export const E2B_MAX_CONNECTIONS = 4096;
36
+ let warnedQueued = false;
37
+ /** Said once per process, the first time a call has to wait for a
38
+ * connection: E2B never holds a call back, so a caller should hear it. */
39
+ export function warnQueued(maxConnections) {
40
+ if (warnedQueued || typeof process === "undefined" || !process.emitWarning)
41
+ return;
42
+ warnedQueued = true;
43
+ process.emitWarning(`More than ${maxConnections} calls and command streams are open at once from this process, so the rest wait their turn. ` +
44
+ "Pass runtime: { client: new Runtime({ maxConnections }) } for more.", { code: "RUNTIME_E2B_QUEUED" });
45
+ }
29
46
  /** The Runtime client for these options: an explicit client, else one per key,
30
47
  * made once and reused so its connections stay open. */
31
48
  export function clientFor(opts = {}) {
@@ -36,30 +53,67 @@ export function clientFor(opts = {}) {
36
53
  const cacheKey = key ?? "";
37
54
  let client = clients.get(cacheKey);
38
55
  if (!client) {
39
- client = new Runtime(key ? { apiKey: key } : {});
56
+ client = new Runtime({
57
+ ...(key ? { apiKey: key } : {}),
58
+ maxConnections: E2B_MAX_CONNECTIONS,
59
+ onQueued: warnQueued,
60
+ });
40
61
  clients.set(cacheKey, client);
41
62
  }
42
63
  return client;
43
64
  }
44
65
  /** Explicit key, then RUNTIME_API_KEY, then E2B_API_KEY when it holds a
45
- * Runtime key; undefined lets the Runtime SDK find the saved login. */
66
+ * Runtime key; undefined, when none is set, lets the Runtime SDK find the
67
+ * saved login. An E2B key is refused, never sent. */
46
68
  export function pickKey(explicit) {
47
69
  const runtimeKey = env("RUNTIME_API_KEY");
48
70
  if (explicit !== undefined && !isE2BKey(explicit))
49
71
  return explicit;
50
- if (runtimeKey)
72
+ if (runtimeKey) {
73
+ warnTwoKeys(runtimeKey, env("E2B_API_KEY"));
51
74
  return runtimeKey;
75
+ }
52
76
  if (explicit !== undefined)
53
77
  throw new AuthenticationError("The apiKey passed is an E2B API key (e2b_...), and it was not sent. Set RUNTIME_API_KEY to a Runtime key " +
54
78
  "(https://withruntime.com/account/keys, or run `npx withruntime login`), or pass the Runtime key as apiKey.");
55
- // An E2B key left in E2B_API_KEY is skipped, never sent: the Runtime SDK
56
- // then uses the key `npx withruntime login` saved, or says none was found.
79
+ // An E2B key left in E2B_API_KEY is never sent, and never quietly passed
80
+ // over for the key `npx withruntime login` saved, which may be another
81
+ // account's.
57
82
  const e2bVariable = env("E2B_API_KEY");
58
- return e2bVariable && !isE2BKey(e2bVariable) ? e2bVariable : undefined;
83
+ if (e2bVariable && isE2BKey(e2bVariable))
84
+ throw new AuthenticationError("E2B_API_KEY holds an E2B API key (e2b_...), and it was not sent. Set RUNTIME_API_KEY to a Runtime key " +
85
+ "(https://withruntime.com/account/keys, or run `npx withruntime login`), or put the Runtime key in E2B_API_KEY.");
86
+ return e2bVariable || undefined;
87
+ }
88
+ /** Where the key in use came from, as an error names it. */
89
+ export function keySource(explicit) {
90
+ if (explicit !== undefined && !isE2BKey(explicit))
91
+ return "apiKey";
92
+ if (env("RUNTIME_API_KEY"))
93
+ return "RUNTIME_API_KEY";
94
+ if (env("E2B_API_KEY"))
95
+ return "E2B_API_KEY";
96
+ return "the saved login";
97
+ }
98
+ let warnedTwoKeys = false;
99
+ /** Said once per process: RUNTIME_API_KEY wins, as in Runtime's own SDK, but
100
+ * code written for E2B means E2B_API_KEY, so two different Runtime keys are
101
+ * likely two accounts, and the wrong one fails later as a missing image
102
+ * (5 October 2026). */
103
+ function warnTwoKeys(runtimeKey, e2bVariable) {
104
+ if (!e2bVariable || isE2BKey(e2bVariable) || e2bVariable === runtimeKey)
105
+ return;
106
+ if (warnedTwoKeys || typeof process === "undefined" || !process.emitWarning)
107
+ return;
108
+ warnedTwoKeys = true;
109
+ process.emitWarning("RUNTIME_API_KEY and E2B_API_KEY hold different Runtime keys, which may be different accounts; this package uses " +
110
+ "RUNTIME_API_KEY. Unset one, or set both to the same key.", { code: "RUNTIME_E2B_TWO_KEYS" });
59
111
  }
60
- /** Test hook: forget the cached clients. */
112
+ /** Test hook: forget the cached clients and the warnings. */
61
113
  export function resetClients() {
62
114
  clients.clear();
115
+ warnedQueued = false;
116
+ warnedTwoKeys = false;
63
117
  }
64
118
  /** Request options for a Runtime call from E2B's. */
65
119
  export function request(opts = {}) {
@@ -43,6 +43,9 @@ export interface SandboxContext {
43
43
  /** Makes /home/user (E2B's home) lead to /workspace (Runtime's), once, when
44
44
  * something names it. */
45
45
  ensureHome(text: string | undefined, options?: RequestOptions): Promise<void>;
46
+ /** Whether /home/user leads to /workspace, linking it first if nothing is
47
+ * there; false when that cannot be told. Never fails. */
48
+ homeIsWorkspace(): Promise<boolean>;
46
49
  }
47
50
  /** E2B's pids are numbers; Runtime's process ids are strings. The number is a
48
51
  * 31-bit FNV-1a hash of the id, so every client derives the same one. */
@@ -1,5 +1,5 @@
1
- import { CommandExitError, guard, InvalidArgumentError, SandboxError, TimeoutError, translate, } from "./errors.js";
2
- import { commandAs, runAs } from "./users.js";
1
+ import { CommandExitError, guard, InvalidArgumentError, SandboxError, SandboxNotFoundError, TimeoutError, translate, } from "./errors.js";
2
+ import { commandAs, listedAs, runAs } from "./users.js";
3
3
  const DEFAULT_TIMEOUT_MS = 60_000;
4
4
  /** How long a command may run on Runtime: a day, E2B's longest sandbox. E2B's
5
5
  * own `timeoutMs` bounds only the connection, so the process gets this. */
@@ -238,12 +238,19 @@ export class Commands {
238
238
  function describe(info) {
239
239
  return {
240
240
  pid: pidOf(info.id),
241
- cmd: "/bin/bash",
242
- args: ["-c", info.command],
241
+ ...listedAs(info.command),
243
242
  envs: {},
244
243
  cwd: info.cwd,
245
244
  };
246
245
  }
246
+ /** A command whose sandbox went away under it, as E2B says it: a
247
+ * TimeoutError, which code written for E2B catches (5 October 2026: it was a
248
+ * SandboxNotFoundError). */
249
+ function sandboxEnded(cause) {
250
+ const error = new TimeoutError("The command ended before the stream completed: the sandbox was killed or reached its end of life.");
251
+ error.cause = cause;
252
+ return error;
253
+ }
247
254
  /** Reader cancellation also releases a callback whose promise never settles. */
248
255
  async function deliver(signal, callback) {
249
256
  signal.throwIfAborted();
@@ -349,7 +356,11 @@ export class CommandHandle {
349
356
  if (!this.#disconnected && !this.#failure) {
350
357
  const translated = translate(error, "sandbox");
351
358
  this.#failure =
352
- translated instanceof Error ? translated : new SandboxError(String(translated));
359
+ translated instanceof SandboxNotFoundError
360
+ ? sandboxEnded(translated)
361
+ : translated instanceof Error
362
+ ? translated
363
+ : new SandboxError(String(translated));
353
364
  }
354
365
  }
355
366
  finally {
@@ -37,6 +37,31 @@ export declare class ServiceBusyError extends Error {
37
37
  requestId?: string;
38
38
  constructor(message: string);
39
39
  }
40
+ /** E2B's other error classes, by the same names and parents, so a module
41
+ * that imports or catches them loads and works: nothing in this package
42
+ * throws them, since the features behind them (template builds, git,
43
+ * volumes, secrets) answer NotSupportedError here. */
44
+ export declare class GitAuthError extends AuthenticationError {
45
+ }
46
+ export declare class GitUpstreamError extends SandboxError {
47
+ }
48
+ export declare class BuildError extends Error {
49
+ constructor(message?: string);
50
+ }
51
+ export declare class FileUploadError extends BuildError {
52
+ }
53
+ export declare class VolumeError extends Error {
54
+ constructor(message?: string);
55
+ }
56
+ export declare class VolumeNotFoundError extends VolumeError {
57
+ }
58
+ export declare class VolumePathNotFoundError extends VolumeError {
59
+ }
60
+ export declare class SecretError extends Error {
61
+ constructor(message?: string);
62
+ }
63
+ export declare class SecretNotFoundError extends SecretError {
64
+ }
40
65
  /** A call that E2B supports and Runtime does not, or not in the same way.
41
66
  * Thrown before anything is done, never after doing something different.
42
67
  * `feature` names what was asked for; `alternative` says what to use on
@@ -46,8 +71,7 @@ export declare class NotSupportedError extends SandboxError {
46
71
  readonly alternative: string;
47
72
  constructor(feature: string, alternative: string, message?: string);
48
73
  }
49
- /** Runtime's addition, for `getHost` and `getPublicHost`: a free-trial
50
- * sandbox shares a port only privately, so a request needs the port's token,
74
+ /** Runtime's addition, for `getHost` and `getPublicHost`: a sandbox without credit shares a port only privately, so a request needs the port's token,
51
75
  * and a host name alone cannot carry one. The message says what works. A
52
76
  * NotSupportedError, so code that catches those catches this too. */
53
77
  export declare class PublicPreviewNotAllowedError extends NotSupportedError {
@@ -58,6 +58,40 @@ export class ServiceBusyError extends Error {
58
58
  this.name = new.target.name;
59
59
  }
60
60
  }
61
+ /** E2B's other error classes, by the same names and parents, so a module
62
+ * that imports or catches them loads and works: nothing in this package
63
+ * throws them, since the features behind them (template builds, git,
64
+ * volumes, secrets) answer NotSupportedError here. */
65
+ export class GitAuthError extends AuthenticationError {
66
+ }
67
+ export class GitUpstreamError extends SandboxError {
68
+ }
69
+ export class BuildError extends Error {
70
+ constructor(message) {
71
+ super(message);
72
+ this.name = new.target.name;
73
+ }
74
+ }
75
+ export class FileUploadError extends BuildError {
76
+ }
77
+ export class VolumeError extends Error {
78
+ constructor(message) {
79
+ super(message);
80
+ this.name = new.target.name;
81
+ }
82
+ }
83
+ export class VolumeNotFoundError extends VolumeError {
84
+ }
85
+ export class VolumePathNotFoundError extends VolumeError {
86
+ }
87
+ export class SecretError extends Error {
88
+ constructor(message) {
89
+ super(message);
90
+ this.name = new.target.name;
91
+ }
92
+ }
93
+ export class SecretNotFoundError extends SecretError {
94
+ }
61
95
  /** A call that E2B supports and Runtime does not, or not in the same way.
62
96
  * Thrown before anything is done, never after doing something different.
63
97
  * `feature` names what was asked for; `alternative` says what to use on
@@ -72,19 +106,18 @@ export class NotSupportedError extends SandboxError {
72
106
  this.code = "not_supported";
73
107
  }
74
108
  }
75
- /** Runtime's addition, for `getHost` and `getPublicHost`: a free-trial
76
- * sandbox shares a port only privately, so a request needs the port's token,
109
+ /** Runtime's addition, for `getHost` and `getPublicHost`: a sandbox without credit shares a port only privately, so a request needs the port's token,
77
110
  * and a host name alone cannot carry one. The message says what works. A
78
111
  * NotSupportedError, so code that catches those catches this too. */
79
112
  export class PublicPreviewNotAllowedError extends NotSupportedError {
80
113
  constructor(sandboxId, port, message) {
81
114
  const where = port === undefined ? "<port>" : String(port);
82
- const alternative = `For an address that works on the trial, use (await sandbox.runtime.previews.create(${where})).urlWithToken: ` +
115
+ const alternative = `For an address that works without credit, use (await sandbox.runtime.previews.create(${where})).urlWithToken: ` +
83
116
  "it carries the token, in a browser, fetch or curl. For other paths on it, send the token as the " +
84
117
  "x-runtime-preview-token header or the runtime_preview_token query parameter. A public host needs a paid " +
85
118
  "sandbox, which is the account owner's decision.";
86
- super("A public address on a free-trial sandbox", alternative, message ??
87
- `Sandbox ${sandboxId} runs on the free trial, where a shared port is private: every request needs the ` +
119
+ super("A public address on a sandbox without credit", alternative, message ??
120
+ `Sandbox ${sandboxId} runs without credit, where a shared port is private: every request needs the ` +
88
121
  `port's token, and a host name alone cannot carry one. ${alternative}`);
89
122
  this.code = "public_preview_not_allowed";
90
123
  }
@@ -72,7 +72,7 @@ export interface FilesystemListOpts extends FilesystemRequestOpts {
72
72
  }
73
73
  /** `sandbox.files`: E2B's filesystem module over Runtime's files API. Relative
74
74
  * paths resolve against the home directory, as in E2B (Runtime's is
75
- * /workspace). */
75
+ * /workspace), and come back under /home/user, as E2B gives them. */
76
76
  export declare class Filesystem {
77
77
  #private;
78
78
  constructor(ctx: SandboxContext);