withruntime 0.11.1 → 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 (53) hide show
  1. package/BLAXEL.md +1 -1
  2. package/DAYTONA.md +1 -1
  3. package/E2B.md +57 -29
  4. package/README.md +4 -3
  5. package/README.src.md +4 -3
  6. package/compatibility-lock.json +14 -14
  7. package/dist/blaxel/errors.js +4 -4
  8. package/dist/blaxel/sandbox.d.ts +1 -2
  9. package/dist/blaxel/sandbox.js +1 -2
  10. package/dist/cli.d.ts +1 -1
  11. package/dist/cli.js +63 -15
  12. package/dist/client.d.ts +11 -59
  13. package/dist/client.js +8 -113
  14. package/dist/daytona/daytona.d.ts +1 -2
  15. package/dist/daytona/daytona.js +1 -2
  16. package/dist/daytona/sandbox.d.ts +1 -1
  17. package/dist/daytona/sandbox.js +1 -1
  18. package/dist/e2b/client.d.ts +15 -2
  19. package/dist/e2b/client.js +61 -7
  20. package/dist/e2b/commands.d.ts +3 -0
  21. package/dist/e2b/commands.js +16 -5
  22. package/dist/e2b/errors.d.ts +26 -2
  23. package/dist/e2b/errors.js +38 -5
  24. package/dist/e2b/filesystem.d.ts +1 -1
  25. package/dist/e2b/filesystem.js +82 -16
  26. package/dist/e2b/index.d.ts +2 -2
  27. package/dist/e2b/index.js +2 -2
  28. package/dist/e2b/sandbox.d.ts +23 -14
  29. package/dist/e2b/sandbox.js +195 -178
  30. package/dist/e2b/unsupported.d.ts +19 -0
  31. package/dist/e2b/unsupported.js +21 -0
  32. package/dist/e2b/users.d.ts +9 -0
  33. package/dist/e2b/users.js +22 -0
  34. package/dist/errors.d.ts +4 -4
  35. package/dist/errors.js +12 -10
  36. package/dist/machine.d.ts +180 -0
  37. package/dist/machine.js +504 -0
  38. package/dist/openai-agents/index.d.ts +1 -1
  39. package/dist/openai-agents/index.js +1 -1
  40. package/dist/products/jobs.d.ts +2 -3
  41. package/dist/products/jobs.js +1 -2
  42. package/dist/products/limits.d.ts +4 -10
  43. package/dist/products/network.d.ts +1 -1
  44. package/dist/products/secrets.d.ts +1 -1
  45. package/dist/products/switching.d.ts +2 -3
  46. package/dist/sandbox.d.ts +7 -92
  47. package/dist/sandbox.js +8 -362
  48. package/dist/transport.d.ts +4 -2
  49. package/dist/transport.js +6 -4
  50. package/dist/types.d.ts +53 -8
  51. package/dist/vercel/sandbox.d.ts +1 -2
  52. package/dist/vercel/sandbox.js +1 -2
  53. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { clientFor, request, } from "./client.js";
1
+ import { clientFor, keySource, request, } from "./client.js";
2
2
  import { Commands } from "./commands.js";
3
3
  import { guard, InvalidArgumentError, NotSupportedError, PublicPreviewNotAllowedError, SandboxError, SandboxNotFoundError, TemplateError, } from "./errors.js";
4
4
  import { Filesystem } from "./filesystem.js";
@@ -9,15 +9,22 @@ 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. 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. */
12
+ /** E2B's default sandbox timeout, 300 s. A sandbox created without timeoutMs
13
+ * gets it, and is paused when it runs out rather than killed, since nobody
14
+ * asked for it to end: nothing in it is lost, and the next call wakes it. On
15
+ * a pilot account the server keeps every sandbox running instead (0254). */
15
16
  export const DEFAULT_TIMEOUT_MS = 300_000;
16
- /** Runtime's lease bounds (MAX_TIMEOUT_SECONDS in the API). */
17
- const MIN_LEASE_SECONDS = 60;
18
- const MAX_LEASE_SECONDS = 3600;
19
- /** E2B's longest sandbox timeout, 24 hours (its Pro plan). */
17
+ /** Runtime's time limit bounds (MIN_TIMEOUT_SECONDS and MAX_TIMEOUT_SECONDS
18
+ * in the API): a minute to 24 hours, E2B's longest (its Pro plan). The server
19
+ * keeps the deadline: nothing in this process has to stay running for it. */
20
+ const MIN_TIMEOUT_SECONDS = 60;
20
21
  const LONGEST_TIMEOUT_MS = 86_400_000;
22
+ /** Makes E2B's home lead to Runtime's unless the image has its own, and
23
+ * prints "same" when /home/user is then /workspace. */
24
+ const HOME_LINK = "[ -e /home/user ] || sudo ln -s /workspace /home/user || exit 1; if [ /home/user -ef /workspace ]; then echo same; fi";
25
+ /** The label that keeps the template a sandbox was made from, so getInfo
26
+ * and list name it whichever client asks; it is not shown in `metadata`. */
27
+ export const TEMPLATE_LABEL = "e2b-template";
21
28
  /** E2B template names that mean "the stock environment": Runtime's default image. */
22
29
  const STOCK_TEMPLATES = new Set(["base", "code-interpreter-v1", "code-interpreter"]);
23
30
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -28,17 +35,17 @@ function checkForkCount(count) {
28
35
  if (count !== undefined && (!Number.isInteger(count) || count < 1 || count > MAX_FORK_COUNT))
29
36
  throw new InvalidArgumentError(`count must be an integer between 1 and ${MAX_FORK_COUNT}`);
30
37
  }
31
- /** The first lease for `timeoutMs`: up to an hour; `keepUntil` carries a
32
- * longer timeout on from there. */
33
- function leaseSeconds(timeoutMs) {
38
+ /** Runtime's time limit for `timeoutMs`, in whole seconds: at least a
39
+ * minute (with a warning once), at most 24 hours. */
40
+ function timeoutSeconds(timeoutMs) {
34
41
  checkTimeout(timeoutMs);
35
- const seconds = Math.min(Math.ceil(timeoutMs / 1000), MAX_LEASE_SECONDS);
36
- if (seconds < MIN_LEASE_SECONDS) {
42
+ const seconds = Math.ceil(timeoutMs / 1000);
43
+ if (seconds < MIN_TIMEOUT_SECONDS) {
37
44
  if (!warnedShortLease && typeof process !== "undefined") {
38
45
  warnedShortLease = true;
39
- process.emitWarning(`timeoutMs ${timeoutMs} is under Runtime's shortest lease; the sandbox gets 60 s. Call kill() when done.`, { code: "RUNTIME_E2B_SHORT_TIMEOUT" });
46
+ process.emitWarning(`timeoutMs ${timeoutMs} is under Runtime's shortest time limit; the sandbox gets 60 s. Call kill() when done.`, { code: "RUNTIME_E2B_SHORT_TIMEOUT" });
40
47
  }
41
- return MIN_LEASE_SECONDS;
48
+ return MIN_TIMEOUT_SECONDS;
42
49
  }
43
50
  return seconds;
44
51
  }
@@ -48,88 +55,18 @@ function checkTimeout(timeoutMs) {
48
55
  if (timeoutMs > LONGEST_TIMEOUT_MS)
49
56
  throw new InvalidArgumentError(`timeoutMs is at most 24 hours (86_400_000 ms), as on E2B, not ${timeoutMs}.`);
50
57
  }
51
- /* Timeouts over an hour. Runtime's lease reaches at most an hour ahead of
52
- now, and E2B's timeout up to a day. While this process runs, the adapter
53
- moves the lease on every five minutes, as far as an hour ahead and never
54
- past the end asked for. One keeper per sandbox, whichever object asked.
55
- If the process ends first, the sandbox ends when its lease does, at most
56
- an hour later; it is never kept past the time asked for. */
57
- const KEEP_EVERY_MS = 5 * 60_000;
58
- /** Kept under the API's hour, for the clocks and the trip. */
59
- const KEEP_AHEAD_MS = (MAX_LEASE_SECONDS - 30) * 1000;
60
- const keepers = new Map();
61
- let warnedKeeper = false;
62
- /** Keeps `runtime` until `until` (epoch ms), or stops keeping it when that
63
- * is within the hour its lease can already reach. */
64
- function keepUntil(runtime, until) {
65
- const keeper = keepers.get(runtime.id);
66
- if (until - Date.now() <= MAX_LEASE_SECONDS * 1000)
67
- return stopKeeping(runtime.id);
68
- if (keeper) {
69
- keeper.until = until;
70
- keeper.runtime = runtime;
71
- return;
72
- }
73
- const made = { until, runtime };
74
- keepers.set(runtime.id, made);
75
- schedule(made);
76
- }
77
- function stopKeeping(sandboxId) {
78
- const keeper = keepers.get(sandboxId);
79
- if (keeper?.timer !== undefined)
80
- clearTimeout(keeper.timer);
81
- keepers.delete(sandboxId);
82
- }
83
- function schedule(keeper) {
84
- keeper.timer = setTimeout(() => void renew(keeper), KEEP_EVERY_MS);
85
- // Never what keeps a Node or Bun process running.
86
- keeper.timer.unref?.();
87
- }
88
- async function renew(keeper) {
89
- const id = keeper.runtime.id;
90
- if (keepers.get(id) !== keeper)
91
- return;
92
- try {
93
- await keeper.runtime.refresh();
94
- const current = state(keeper.runtime);
95
- if (current === "stopped")
96
- return stopKeeping(id);
97
- if (current === "running") {
98
- if (keeper.runtime.info.endsAt === null)
99
- return stopKeeping(id);
100
- const end = Date.parse(keeper.runtime.info.expiresAt);
101
- const want = Math.min(keeper.until, Date.now() + KEEP_AHEAD_MS);
102
- const later = Math.floor((want - end) / 1000);
103
- if (later >= 1)
104
- await keeper.runtime.extend(later);
105
- if (Date.parse(keeper.runtime.info.expiresAt) >= keeper.until - 1000)
106
- return stopKeeping(id);
107
- }
108
- }
109
- catch (error) {
110
- if (!warnedKeeper && typeof process !== "undefined" && process.emitWarning) {
111
- warnedKeeper = true;
112
- process.emitWarning(`Could not extend sandbox ${id}'s lease toward its timeout: ${error instanceof Error ? error.message : String(error)}. Trying again in five minutes.`, { code: "RUNTIME_E2B_LEASE" });
113
- }
114
- }
115
- if (keepers.get(id) === keeper)
116
- schedule(keeper);
117
- }
118
- /** Test hook: renew every kept lease now, as the five-minute timer would. */
119
- export async function renewLeases() {
120
- await Promise.all([...keepers.values()].map((keeper) => renew(keeper)));
121
- }
122
- /** Test hook: the end each kept sandbox is kept until. */
123
- export function keptLeases() {
124
- return new Map([...keepers].map(([id, keeper]) => [id, keeper.until]));
125
- }
126
- function onLeaseEnd(lifecycle) {
58
+ /** What the server does when the time limit runs out. E2B kills: a caller
59
+ * that asked for an end (a timeoutMs, or onTimeout "kill") gets a delete, as
60
+ * E2B's kill destroys the sandbox. One that asked for nothing gets E2B's
61
+ * 300 s default as a pause, which keeps everything, since a sandbox nobody
62
+ * asked to end is never ended. */
63
+ function onTimeout(lifecycle, timeoutAsked) {
127
64
  if (!lifecycle)
128
- return "stop";
65
+ return timeoutAsked ? "delete" : "pause";
129
66
  const action = typeof lifecycle.onTimeout === "string" ? lifecycle.onTimeout : lifecycle.onTimeout.action;
130
67
  if (typeof lifecycle.onTimeout === "object" && lifecycle.onTimeout.keepMemory === false)
131
68
  throw new NotSupportedError("A files-only pause (keepMemory: false)", "Runtime's pause keeps memory and files; leave keepMemory out.");
132
- return action === "pause" ? "pause" : "stop";
69
+ return action === "pause" ? "pause" : "delete";
133
70
  }
134
71
  function refuseCreate(opts) {
135
72
  const refusals = [
@@ -154,8 +91,16 @@ function refuseCreate(opts) {
154
91
  if (opts[field] !== undefined)
155
92
  throw new NotSupportedError(feature, alternative);
156
93
  }
94
+ /** The Runtime image name an E2B template name stands for: E2B's
95
+ * `team/name:tag` without the team, since a Runtime account's images are its
96
+ * own. `name`, `name:tag` and `name@version` are Runtime image names too. */
97
+ export function imageNameFor(template) {
98
+ const slash = template.lastIndexOf("/");
99
+ return slash < 0 ? template : template.slice(slash + 1);
100
+ }
157
101
  /** Where a template sends the create: the stock image, a Runtime image by
158
- * name or id, or a Runtime snapshot by id. */
102
+ * name (with its tag or version) or id, or a Runtime snapshot by id. A name
103
+ * is resolved by the create itself, in the same request. */
159
104
  async function resolveTemplate(client, template, options) {
160
105
  options.signal?.throwIfAborted();
161
106
  if (STOCK_TEMPLATES.has(template))
@@ -171,16 +116,36 @@ async function resolveTemplate(client, template, options) {
171
116
  }
172
117
  return { snapshot: template };
173
118
  }
174
- const page = await client.images.list({ name: template, state: "ready", limit: 1 }, options);
175
- const image = page.data[0];
176
- if (image)
177
- return { image: image.id };
178
- const error = new TemplateError(`No Runtime image is named "${template}". E2B templates do not run on Runtime; build the same ` +
119
+ return { image: imageNameFor(template) };
120
+ }
121
+ /** The labels a create sends: E2B's metadata, and the template it was made
122
+ * from, while there is room for it among Runtime's 32 labels. */
123
+ function labelsFor(metadata, template) {
124
+ const labels = { ...metadata };
125
+ if (!STOCK_TEMPLATES.has(template) && Object.keys(labels).length < 32 && template.length <= 256)
126
+ labels[TEMPLATE_LABEL] = template;
127
+ return Object.keys(labels).length ? { labels } : {};
128
+ }
129
+ /** " in the account …", for an error a key of another account explains: the
130
+ * account's name and where its key came from. Nothing when the lookup fails. */
131
+ async function accountOf(client, opts) {
132
+ const me = await client.me({ timeoutMs: 5_000 }).catch(() => undefined);
133
+ if (!me)
134
+ return undefined;
135
+ const source = opts.runtime?.client ? "" : ` (key: ${keySource(opts.apiKey)})`;
136
+ return ` in the account "${me.orgName || me.orgId}"${source}`;
137
+ }
138
+ /** The create's refusal of a template no Runtime image answers to, as E2B's
139
+ * TemplateError, saying how to build it. */
140
+ function templateMissing(template, cause, account) {
141
+ const name = imageNameFor(template).replace(/[:@].*$/, "");
142
+ const error = new TemplateError(`No Runtime image is named "${imageNameFor(template)}"${account ?? ""}. E2B templates do not run on Runtime; build the same ` +
179
143
  `environment as a Runtime image with that name and this call starts from it: ` +
180
- `\`npx withruntime image build --dockerfile e2b.Dockerfile --name ${template}\`, or ` +
181
- `runtime.images.build({ name: "${template}", dockerfile }).`);
144
+ `\`npx withruntime image build --dockerfile e2b.Dockerfile --name ${name}\`, or ` +
145
+ `runtime.images.build({ name: "${name}", dockerfile }).`);
182
146
  error.code = "template_not_found";
183
- throw error;
147
+ error.cause = cause;
148
+ return error;
184
149
  }
185
150
  function state(runtime) {
186
151
  const current = runtime.state;
@@ -190,28 +155,57 @@ function state(runtime) {
190
155
  return "stopped";
191
156
  return "running";
192
157
  }
193
- function infoOf(runtime) {
158
+ /** Whether a paused sandbox is, to code written for E2B, still running:
159
+ * Runtime paused it itself for being idle (E2B never does) and the next
160
+ * call wakes it, so every E2B call on it works as on a running one. */
161
+ function idleAsleep(runtime) {
194
162
  const info = runtime.info;
195
- const templateId = typeof info.image === "string"
196
- ? info.image
197
- : typeof info.snapshot === "string"
198
- ? info.snapshot
199
- : "base";
163
+ return state(runtime) === "paused" && info.stopReason === "idle" && info.autoWake !== false;
164
+ }
165
+ /** The sandbox's state as E2B would say it. */
166
+ function e2bState(runtime) {
200
167
  const current = state(runtime);
168
+ if (current === "running" || idleAsleep(runtime))
169
+ return "running";
170
+ return "paused";
171
+ }
172
+ /** E2B's endAt: when the sandbox ends by itself. One that never will (no
173
+ * time limit, as a persistent or a pilot's sandbox) reads 24 hours ahead, the
174
+ * furthest an E2B sandbox's end may be, so code that waits until the end, or
175
+ * extends near it, behaves as for the longest E2B sandbox: a JavaScript timer
176
+ * set past 24.8 days would fire at once. (5 October 2026: it read where the
177
+ * sandbox was paid up to, minutes ahead, so a pilot's looked about to end.) */
178
+ function endOf(runtime) {
179
+ const { endsAt, expiresAt } = runtime.info;
180
+ if (endsAt)
181
+ return new Date(endsAt);
182
+ if (endsAt === null && e2bState(runtime) === "running")
183
+ return new Date(Date.now() + LONGEST_TIMEOUT_MS);
184
+ return new Date(expiresAt);
185
+ }
186
+ function infoOf(runtime) {
187
+ const info = runtime.info;
188
+ const { [TEMPLATE_LABEL]: template, ...metadata } = info.labels ?? {};
189
+ const templateId = template ??
190
+ (typeof info.image === "string"
191
+ ? info.image
192
+ : typeof info.snapshot === "string"
193
+ ? info.snapshot
194
+ : "base");
195
+ const action = info.onTimeout ?? info.onLeaseEnd;
201
196
  return {
202
197
  sandboxId: info.id,
203
198
  templateId,
204
199
  ...(info.name ? { name: info.name } : {}),
205
- metadata: info.labels ?? {},
200
+ metadata,
206
201
  startedAt: new Date(info.createdAt),
207
- // With no time limit, where it is paid up to: always ahead, moving on.
208
- endAt: new Date(info.endsAt ?? info.expiresAt),
209
- state: current === "stopped" ? "paused" : current,
202
+ endAt: endOf(runtime),
203
+ state: e2bState(runtime),
210
204
  cpuCount: info.vcpu,
211
205
  memoryMB: info.memoryMiB,
212
206
  envdVersion: "runtime",
213
207
  lifecycle: {
214
- onTimeout: info.onLeaseEnd === "stop" ? "kill" : "pause",
208
+ onTimeout: action === "pause" ? "pause" : "kill",
215
209
  autoResume: info.autoWake === true,
216
210
  },
217
211
  };
@@ -231,6 +225,8 @@ export class Sandbox {
231
225
  pty;
232
226
  #client;
233
227
  #runtime;
228
+ #killed = false;
229
+ /** The /home/user link, once asked for: whether /home/user leads to /workspace. */
234
230
  #home;
235
231
  /** Ports shared as public previews, by port: the one share each asked for. */
236
232
  #shares = new Map();
@@ -241,6 +237,7 @@ export class Sandbox {
241
237
  this.#client = client;
242
238
  const ctx = {
243
239
  ensureHome: (text, options) => this.#ensureHome(text, options),
240
+ homeIsWorkspace: () => (this.#home ?? this.#linkHome()).catch(() => false),
244
241
  requestTimeoutMs: this.requestTimeoutMs,
245
242
  };
246
243
  // A getter, so the modules see the sandbox as it is after each refresh.
@@ -283,21 +280,32 @@ export class Sandbox {
283
280
  });
284
281
  });
285
282
  };
283
+ const done = (home) => wait(home.then(() => undefined));
286
284
  if (this.#home || !text?.includes("/home/user"))
287
- return wait(this.#home ?? Promise.resolve());
285
+ return done(this.#home ?? Promise.resolve(false));
286
+ return done(this.#linkHome(signal));
287
+ }
288
+ /** Links /home/user to /workspace unless something is already there, and
289
+ * says whether /home/user then leads to /workspace: an image may have its
290
+ * own /home/user, and E2B's paths are shown only where they are true. */
291
+ #linkHome(signal) {
288
292
  const pending = (async () => {
289
- const result = await guard("sandbox", () => this.#runtime.exec("[ -e /home/user ] || sudo ln -s /workspace /home/user", { signal }));
290
- if (result.exitCode !== 0 && typeof process !== "undefined")
291
- process.emitWarning(`Could not link /home/user to /workspace: ${result.stderr.trim()}`, {
292
- code: "RUNTIME_E2B_HOME",
293
- });
293
+ const result = await guard("sandbox", () => this.#runtime.exec(HOME_LINK, { signal }));
294
+ if (result.exitCode !== 0) {
295
+ if (typeof process !== "undefined")
296
+ process.emitWarning(`Could not link /home/user to /workspace: ${result.stderr.trim()}`, {
297
+ code: "RUNTIME_E2B_HOME",
298
+ });
299
+ return false;
300
+ }
301
+ return result.stdout.trim() === "same";
294
302
  })();
295
303
  this.#home = pending;
296
304
  pending.catch(() => {
297
305
  if (this.#home === pending)
298
306
  this.#home = undefined;
299
307
  });
300
- return wait(pending);
308
+ return pending;
301
309
  }
302
310
  static async create(templateOrOpts, maybeOpts) {
303
311
  const opts = (typeof templateOrOpts === "string" ? maybeOpts : templateOrOpts) ?? {};
@@ -306,15 +314,15 @@ export class Sandbox {
306
314
  options.signal?.throwIfAborted();
307
315
  refuseCreate(opts);
308
316
  const client = clientFor(opts);
309
- // No timeoutMs, no time limit: it runs while it works (0300).
310
- const asked = opts.timeoutMs === undefined ? undefined : Date.now() + opts.timeoutMs;
317
+ // The server keeps the deadline, up to 24 hours (0381). Without one asked
318
+ // for, E2B's 300 s ends in a pause, never a delete.
311
319
  const input = {
312
- ...(opts.timeoutMs === undefined ? {} : { timeoutSeconds: leaseSeconds(opts.timeoutMs) }),
313
- onLeaseEnd: onLeaseEnd(opts.lifecycle),
320
+ timeoutSeconds: timeoutSeconds(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
321
+ onLeaseEnd: onTimeout(opts.lifecycle, opts.timeoutMs !== undefined),
314
322
  // With a lifecycle, E2B resumes a paused sandbox on traffic only when
315
323
  // asked (autoResume); Runtime's automatic wake is the same thing (0093).
316
324
  ...(opts.lifecycle ? { autoWake: opts.lifecycle.autoResume === true } : {}),
317
- ...(opts.metadata && Object.keys(opts.metadata).length ? { labels: opts.metadata } : {}),
325
+ ...labelsFor(opts.metadata, template),
318
326
  // Runtime keeps them with the sandbox: every command, terminal and
319
327
  // interpreter in it gets them, from this client or any other.
320
328
  ...(opts.envs && Object.keys(opts.envs).length ? { env: opts.envs } : {}),
@@ -329,9 +337,11 @@ export class Sandbox {
329
337
  ...source,
330
338
  ...opts.runtime?.create,
331
339
  };
332
- const runtime = await guard("sandbox", () => client.sandboxes.create(create, options));
333
- if (asked !== undefined)
334
- keepUntil(runtime, asked);
340
+ const runtime = await guard("sandbox", () => client.sandboxes.create(create, options)).catch(async (error) => {
341
+ if (error.code === "image_not_found")
342
+ throw templateMissing(template, error, await accountOf(client, opts));
343
+ throw error;
344
+ });
335
345
  return new this(runtime, client, opts.requestTimeoutMs);
336
346
  }
337
347
  /** Connects to a sandbox by id, waking it if it is paused, as E2B does. A
@@ -353,13 +363,17 @@ export class Sandbox {
353
363
  return new SandboxPaginator(clientFor(opts), opts);
354
364
  }
355
365
  // ---- lifecycle --------------------------------------------------------
356
- /** Stops the sandbox. False when it was not found or had already ended. */
366
+ /** Deletes the sandbox for good, as E2B's kill destroys it: its disk,
367
+ * memory and shared ports go. False when it was not found or was already
368
+ * deleted. */
357
369
  static async kill(sandboxId, opts = {}) {
358
370
  const client = clientFor(opts);
359
- stopKeeping(sandboxId);
360
371
  try {
372
+ // Read first: a delete answers the same however often it is sent, and
373
+ // E2B says false for one already gone.
361
374
  const runtime = await guard("sandbox", () => client.sandboxes.get(sandboxId, request(opts)));
362
- return await stop(runtime, opts);
375
+ await guard("sandbox", () => runtime.delete(request(opts)));
376
+ return true;
363
377
  }
364
378
  catch (error) {
365
379
  if (error instanceof SandboxNotFoundError)
@@ -368,9 +382,12 @@ export class Sandbox {
368
382
  }
369
383
  }
370
384
  async kill(opts = {}) {
371
- stopKeeping(this.sandboxId);
385
+ if (this.#killed)
386
+ return false;
372
387
  try {
373
- return await stop(this.#runtime, opts);
388
+ await guard("sandbox", () => this.#runtime.delete(request(opts)));
389
+ this.#killed = true;
390
+ return true;
374
391
  }
375
392
  catch (error) {
376
393
  if (error instanceof SandboxNotFoundError)
@@ -378,10 +395,11 @@ export class Sandbox {
378
395
  throw error;
379
396
  }
380
397
  }
381
- /** Sets the sandbox to end `timeoutMs` from now, up to 24 hours; past an
382
- * hour the lease is moved on while this process runs. Runtime cannot end a
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. */
398
+ /** Sets the sandbox to end `timeoutMs` from now, up to 24 hours, as E2B
399
+ * does; the server keeps the deadline. Runtime cannot bring an end
400
+ * sooner, so that is refused. A sandbox with no time limit (one on a pilot
401
+ * account, kept running by the server) has no end to move, and nothing
402
+ * changes. */
385
403
  static async setTimeout(sandboxId, timeoutMs, opts = {}) {
386
404
  const client = clientFor(opts);
387
405
  const runtime = await guard("sandbox", () => client.sandboxes.get(sandboxId, request(opts)));
@@ -399,6 +417,9 @@ export class Sandbox {
399
417
  await guard("sandbox", () => this.#runtime.refresh(request(opts)));
400
418
  return infoOf(this.#runtime);
401
419
  }
420
+ /** Whether the sandbox answers calls, as E2B's health check says: true
421
+ * while it runs, and while Runtime has paused it for being idle, since the
422
+ * next call wakes it. False once paused on request, ended or deleted. */
402
423
  async isRunning(opts = {}) {
403
424
  try {
404
425
  await guard("sandbox", () => this.#runtime.refresh(request(opts)));
@@ -408,7 +429,7 @@ export class Sandbox {
408
429
  return false;
409
430
  throw error;
410
431
  }
411
- return this.#runtime.state === "running";
432
+ return this.#runtime.state === "running" || idleAsleep(this.#runtime);
412
433
  }
413
434
  /** Pauses the sandbox, keeping memory and files. False when it was paused. */
414
435
  static async pause(sandboxId, opts = {}) {
@@ -479,7 +500,7 @@ export class Sandbox {
479
500
  getHost(port) {
480
501
  if (!Number.isInteger(port) || port < 1 || port > 65_535)
481
502
  throw new InvalidArgumentError(`port must be a whole number from 1 to 65535, not ${port}.`);
482
- // A trial sandbox's ports are private: a host alone would answer 404 or
503
+ // A sandbox without credit shares ports privately: a host alone would answer 404 or
483
504
  // 401 to everyone. Say so now, with the address that works.
484
505
  if (this.#runtime.info.funding === "trial")
485
506
  throw new PublicPreviewNotAllowedError(this.sandboxId, port);
@@ -546,44 +567,38 @@ async function resume(runtime, opts) {
546
567
  const current = state(runtime);
547
568
  if (current === "stopped")
548
569
  throw new SandboxNotFoundError(`Sandbox ${runtime.id} has ended.`);
549
- const asked = opts.timeoutMs === undefined ? undefined : Date.now() + opts.timeoutMs;
550
570
  if (current === "paused") {
551
571
  await guard("sandbox", () => runtime.wake({
552
- ...(opts.timeoutMs === undefined ? {} : { timeoutSeconds: leaseSeconds(opts.timeoutMs) }),
572
+ ...(opts.timeoutMs === undefined ? {} : { timeoutSeconds: timeoutSeconds(opts.timeoutMs) }),
553
573
  ...request(opts),
554
574
  }));
555
575
  }
556
576
  else if (opts.timeoutMs !== undefined) {
557
577
  checkTimeout(opts.timeoutMs);
558
- // No time limit: no end to move (0300).
559
- if (runtime.info.endsAt === null)
560
- return;
561
- const later = Math.min(asked, Date.now() + KEEP_AHEAD_MS) - Date.parse(runtime.info.expiresAt);
562
- if (later > 1000)
563
- await guard("sandbox", () => runtime.extend(Math.ceil(later / 1000), request(opts)));
564
- }
565
- if (asked !== undefined && asked > (keepers.get(runtime.id)?.until ?? 0))
566
- keepUntil(runtime, asked);
578
+ // A running one's end moves later, never sooner, as E2B's connect.
579
+ const later = secondsLater(runtime, opts.timeoutMs);
580
+ if (later > 1)
581
+ await guard("sandbox", () => runtime.extend(Math.ceil(later), request(opts)));
582
+ }
567
583
  }
568
- async function stop(runtime, opts) {
569
- if (state(runtime) === "stopped")
570
- return false;
571
- await guard("sandbox", () => runtime.stop({ wait: false, ...request(opts) }));
572
- return true;
584
+ /** How many seconds past its current end `now + timeoutMs` is; 0 for a
585
+ * sandbox with no time limit, which has no end to move. A second either way
586
+ * is the clocks and the trip, not a new end. */
587
+ function secondsLater(runtime, timeoutMs) {
588
+ if (runtime.info.endsAt === null)
589
+ return 0;
590
+ const end = Date.parse(runtime.info.endsAt ?? runtime.info.expiresAt);
591
+ return (Date.now() + timeoutMs - end) / 1000;
573
592
  }
574
593
  async function extendTo(runtime, timeoutMs, opts) {
575
594
  checkTimeout(timeoutMs);
576
- // No time limit: no end to move (0300).
577
595
  if (runtime.info.endsAt === null)
578
596
  return;
579
- const asked = Date.now() + timeoutMs;
580
- const end = Date.parse(runtime.info.endsAt ?? runtime.info.expiresAt);
581
- if (asked - end < -1000)
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.");
583
- const later = Math.min(asked, Date.now() + KEEP_AHEAD_MS) - end;
584
- if (later > 1000)
585
- await guard("sandbox", () => runtime.extend(Math.ceil(later / 1000), request(opts)));
586
- keepUntil(runtime, asked);
597
+ const later = secondsLater(runtime, timeoutMs);
598
+ if (later < -1)
599
+ throw new NotSupportedError("Ending a sandbox sooner than its time limit (a shorter setTimeout)", "Runtime cannot bring a time limit forward. Call kill() when the work is done.");
600
+ if (later > 1)
601
+ await guard("sandbox", () => runtime.extend(Math.ceil(later), request(opts)));
587
602
  }
588
603
  async function pause(runtime, opts) {
589
604
  if (opts.keepMemory === false)
@@ -609,6 +624,10 @@ export class SandboxPaginator {
609
624
  #opts;
610
625
  #pageSize;
611
626
  #local;
627
+ /** One E2B state asked for alone: Runtime's states are read together and
628
+ * sorted here, since a sandbox Runtime paused for being idle is running
629
+ * to E2B. */
630
+ #only;
612
631
  #page;
613
632
  #all;
614
633
  #offset;
@@ -627,9 +646,10 @@ export class SandboxPaginator {
627
646
  query.startedAfter !== undefined ||
628
647
  opts.order === "desc" ||
629
648
  opts.nextToken !== undefined;
630
- const states = query.state?.length ? query.state : ["running", "paused"];
649
+ const asked = new Set(query.state?.length ? query.state : ["running", "paused"]);
650
+ this.#only = asked.size === 1 ? [...asked][0] : undefined;
631
651
  this.#filter = {
632
- state: states.flatMap((one) => (one === "running" ? [...RUNNING] : [...PAUSED])),
652
+ state: [...RUNNING, ...PAUSED],
633
653
  ...(query.metadata && Object.keys(query.metadata).length ? { labels: query.metadata } : {}),
634
654
  limit: this.#local ? 100 : Math.min(this.#pageSize, 100),
635
655
  };
@@ -658,21 +678,28 @@ export class SandboxPaginator {
658
678
  : await this.#client.sandboxes.list(this.#filter, this.#request));
659
679
  this.#page = page ?? undefined;
660
680
  this.#hasNext = Boolean(page?.hasMore);
661
- const items = (page?.data ?? []).map(infoOf);
681
+ const items = (page?.data ?? []).map(infoOf).filter((info) => this.#wanted(info));
662
682
  this.#offset += items.length;
663
683
  return items;
664
684
  }
685
+ #wanted(info) {
686
+ return this.#only === undefined || info.state === this.#only;
687
+ }
665
688
  async #everyMatch() {
666
689
  const query = this.#opts.query ?? {};
667
690
  const templates = query.template === undefined
668
691
  ? undefined
669
- : await templateIds(this.#client, query.template, this.#request);
692
+ : STOCK_TEMPLATES.has(query.template)
693
+ ? new Set(["base", ...STOCK_TEMPLATES])
694
+ : new Set([query.template]);
670
695
  const after = query.startedAfter?.getTime();
671
696
  const all = [];
672
697
  let page = await this.#client.sandboxes.list(this.#filter, this.#request);
673
698
  while (page) {
674
699
  for (const one of page.data) {
675
700
  const info = infoOf(one);
701
+ if (!this.#wanted(info))
702
+ continue;
676
703
  if (templates && !templates.has(info.templateId))
677
704
  continue;
678
705
  if (after !== undefined && info.startedAt.getTime() < after)
@@ -691,16 +718,6 @@ function tokenOffset(token) {
691
718
  throw new InvalidArgumentError(`"${token}" is not a nextToken this package gave; pass the paginator's own nextToken.`);
692
719
  return Number(match[1]);
693
720
  }
694
- /** The template ids a template name stands for: "base" for the stock names,
695
- * the id itself for a UUID, and every Runtime image of that name. */
696
- async function templateIds(client, template, options) {
697
- if (STOCK_TEMPLATES.has(template))
698
- return new Set(["base"]);
699
- if (UUID.test(template))
700
- return new Set([template]);
701
- const page = await client.images.list({ name: template, limit: 100 }, options);
702
- return new Set(page.data.map((image) => image.id));
703
- }
704
721
  async function e2bMetrics(runtime, opts) {
705
722
  const start = opts.start === undefined ? Date.now() - 15 * 60_000 : new Date(opts.start).getTime();
706
723
  const end = opts.end === undefined ? Date.now() : new Date(opts.end).getTime();
@@ -17,3 +17,22 @@ export declare const waitForFile: Unsupported;
17
17
  export declare const waitForTimeout: Unsupported;
18
18
  export declare const Volume: Unsupported;
19
19
  export declare const Secret: Unsupported;
20
+ export declare const SecretPaginator: Unsupported;
21
+ export declare const Git: Unsupported;
22
+ export declare const ReadyCmd: Unsupported;
23
+ export declare const LogEntry: Unsupported;
24
+ export declare const LogEntryStart: Unsupported;
25
+ export declare const LogEntryEnd: Unsupported;
26
+ export declare const defaultBuildLogger: Unsupported;
27
+ export declare const ConnectionConfig: Unsupported;
28
+ export declare const ApiClient: Unsupported;
29
+ export declare const getSignature: Unsupported;
30
+ /** E2B's "every address" network selector, as E2B defines it. */
31
+ export declare const ALL_TRAFFIC = "0.0.0.0/0";
32
+ /** E2B's volume entry types, as E2B defines them. */
33
+ export declare enum VolumeFileType {
34
+ UNKNOWN = "unknown",
35
+ FILE = "file",
36
+ DIRECTORY = "directory",
37
+ SYMLINK = "symlink"
38
+ }
@@ -25,3 +25,24 @@ export const waitForFile = unsupportedExport("waitForFile (a template ready chec
25
25
  export const waitForTimeout = unsupportedExport("waitForTimeout (a template ready check)", TEMPLATES);
26
26
  export const Volume = unsupportedExport("Volume", "Use runtime.volumes (Runtime volumes) and mount one with runtime: { create: { volumes: [{ volumeId, path }] } }.");
27
27
  export const Secret = unsupportedExport("Secret", "Use a Runtime secret: `npx withruntime secrets set NAME --host api.example.com`. The sandbox sees a placeholder, and the egress proxy adds the value on HTTPS to that host.");
28
+ export const SecretPaginator = unsupportedExport("SecretPaginator", "List Runtime secrets with `npx withruntime secrets ls` or runtime.secrets.list().");
29
+ export const Git = unsupportedExport("git module (deprecated by E2B too)", "Run git with sandbox.commands.run('git ...'); git is installed.");
30
+ export const ReadyCmd = unsupportedExport("ReadyCmd (a template ready check)", TEMPLATES);
31
+ export const LogEntry = unsupportedExport("LogEntry (template build logs)", TEMPLATES);
32
+ export const LogEntryStart = unsupportedExport("LogEntryStart (template build logs)", TEMPLATES);
33
+ export const LogEntryEnd = unsupportedExport("LogEntryEnd (template build logs)", TEMPLATES);
34
+ export const defaultBuildLogger = unsupportedExport("defaultBuildLogger (template build logs)", TEMPLATES);
35
+ const ENDPOINTS = "This package talks only to Runtime's API through the sandbox's own methods; sandbox.runtime is the Runtime sandbox underneath.";
36
+ export const ConnectionConfig = unsupportedExport("ConnectionConfig", ENDPOINTS);
37
+ export const ApiClient = unsupportedExport("ApiClient (E2B's REST client)", ENDPOINTS);
38
+ export const getSignature = unsupportedExport("getSignature (signed file URLs)", "Use sandbox.files.read and sandbox.files.write.");
39
+ /** E2B's "every address" network selector, as E2B defines it. */
40
+ export const ALL_TRAFFIC = "0.0.0.0/0";
41
+ /** E2B's volume entry types, as E2B defines them. */
42
+ export var VolumeFileType;
43
+ (function (VolumeFileType) {
44
+ VolumeFileType["UNKNOWN"] = "unknown";
45
+ VolumeFileType["FILE"] = "file";
46
+ VolumeFileType["DIRECTORY"] = "directory";
47
+ VolumeFileType["SYMLINK"] = "symlink";
48
+ })(VolumeFileType || (VolumeFileType = {}));