withruntime 0.8.3 → 0.9.0

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 (92) hide show
  1. package/COMPATIBILITY.md +145 -0
  2. package/E2B.md +23 -13
  3. package/README.md +8 -5
  4. package/README.src.md +8 -5
  5. package/compatibility-lock.json +281 -0
  6. package/dist/ai-harness/index.js +2 -2
  7. package/dist/api-defaults.d.ts +17 -0
  8. package/dist/api-defaults.js +22 -0
  9. package/dist/blaxel/preview.d.ts +1 -1
  10. package/dist/blaxel/preview.js +1 -1
  11. package/dist/blaxel/process.d.ts +1 -1
  12. package/dist/blaxel/process.js +11 -2
  13. package/dist/cli-words.d.ts +27 -0
  14. package/dist/cli-words.js +135 -0
  15. package/dist/cli.js +162 -31
  16. package/dist/client.d.ts +4 -1
  17. package/dist/client.js +8 -2
  18. package/dist/cloudflare/index.d.ts +294 -0
  19. package/dist/cloudflare/index.js +554 -0
  20. package/dist/cloudflare/worker.d.ts +415 -0
  21. package/dist/cloudflare/worker.js +297 -0
  22. package/dist/codesandbox/guest-service.d.ts +31 -0
  23. package/dist/codesandbox/guest-service.js +453 -0
  24. package/dist/codesandbox/index.d.ts +386 -0
  25. package/dist/codesandbox/index.js +1313 -0
  26. package/dist/compat/core.d.ts +39 -0
  27. package/dist/compat/core.js +199 -0
  28. package/dist/compat/files.d.ts +26 -0
  29. package/dist/compat/files.js +115 -0
  30. package/dist/compat/shell.d.ts +4 -0
  31. package/dist/compat/shell.js +165 -0
  32. package/dist/daytona/context.js +2 -1
  33. package/dist/daytona/process.d.ts +1 -1
  34. package/dist/daytona/process.js +27 -4
  35. package/dist/e2b/code-interpreter.d.ts +4 -5
  36. package/dist/e2b/code-interpreter.js +54 -56
  37. package/dist/e2b/commands.d.ts +5 -4
  38. package/dist/e2b/commands.js +37 -29
  39. package/dist/e2b/errors.js +1 -1
  40. package/dist/e2b/filesystem.d.ts +8 -6
  41. package/dist/e2b/filesystem.js +127 -28
  42. package/dist/e2b/sandbox.d.ts +4 -3
  43. package/dist/e2b/sandbox.js +10 -10
  44. package/dist/errors.d.ts +8 -0
  45. package/dist/errors.js +15 -2
  46. package/dist/freestyle/index.d.ts +301 -0
  47. package/dist/freestyle/index.js +310 -0
  48. package/dist/index.d.ts +1 -0
  49. package/dist/mcp.js +2 -2
  50. package/dist/modal/index.d.ts +205 -0
  51. package/dist/modal/index.js +485 -0
  52. package/dist/network-cli.js +1 -1
  53. package/dist/openai-agents/index.js +9 -26
  54. package/dist/products/audit.d.ts +3 -0
  55. package/dist/products/index.d.ts +2 -0
  56. package/dist/products/index.js +2 -0
  57. package/dist/products/interpreter.d.ts +11 -4
  58. package/dist/products/interpreter.js +33 -9
  59. package/dist/products/jobs.d.ts +1 -1
  60. package/dist/products/jobs.js +5 -4
  61. package/dist/products/limits.d.ts +10 -0
  62. package/dist/products/previews.d.ts +10 -2
  63. package/dist/products/previews.js +3 -2
  64. package/dist/products/tailscale.d.ts +57 -0
  65. package/dist/products/tailscale.js +17 -0
  66. package/dist/products/watch.d.ts +25 -4
  67. package/dist/products/watch.js +75 -14
  68. package/dist/runloop/index.d.ts +340 -0
  69. package/dist/runloop/index.js +438 -0
  70. package/dist/runloop/pagination.d.ts +46 -0
  71. package/dist/runloop/pagination.js +82 -0
  72. package/dist/runloop/snapshots.d.ts +52 -0
  73. package/dist/runloop/snapshots.js +244 -0
  74. package/dist/sandbox.d.ts +40 -9
  75. package/dist/sandbox.js +273 -76
  76. package/dist/snapshots.d.ts +11 -1
  77. package/dist/snapshots.js +11 -1
  78. package/dist/sprites/index.d.ts +251 -0
  79. package/dist/sprites/index.js +552 -0
  80. package/dist/tar.d.ts +8 -1
  81. package/dist/tar.js +185 -35
  82. package/dist/tools/index.js +2 -1
  83. package/dist/transport.d.ts +20 -2
  84. package/dist/transport.js +48 -11
  85. package/dist/types.d.ts +52 -3
  86. package/dist/vercel/command.js +12 -1
  87. package/dist/vercel/errors.d.ts +2 -2
  88. package/dist/vercel/errors.js +12 -1
  89. package/dist/vercel/filesystem.d.ts +21 -18
  90. package/dist/vercel/filesystem.js +95 -48
  91. package/dist/vercel/sandbox.js +5 -5
  92. package/package.json +45 -3
@@ -0,0 +1,244 @@
1
+ import { RuntimeError } from "../errors.js";
2
+ import { CompatibilityError, only } from "../compat/core.js";
3
+ import { LongPollAbortError, PollingTimeoutError, MaxAttemptsExceededError, } from "./index.js";
4
+ import { DevboxSnapshotViewsDiskSnapshotsCursorIDPage, PagePromise } from "./pagination.js";
5
+ const PAYLOAD = "compat.runloop.snapshot.";
6
+ export function requestOptions(value) {
7
+ const keys = Object.keys(value);
8
+ return (keys.length > 0 && keys.every((key) => ["signal", "timeout", "idempotencyKey"].includes(key)));
9
+ }
10
+ export function nativeOptions(options) {
11
+ return {
12
+ signal: options.signal,
13
+ timeoutMs: options.timeout,
14
+ idempotencyKey: options.idempotencyKey,
15
+ };
16
+ }
17
+ export function snapshotOptions(body) {
18
+ only("Runloop snapshot", body, ["name", "metadata", "commit_message"]);
19
+ if (body.commit_message != null &&
20
+ (typeof body.commit_message !== "string" || body.commit_message.length > 1000))
21
+ throw new TypeError("commit_message must be a string of at most 1000 characters");
22
+ if (body.name != null && typeof body.name !== "string")
23
+ throw new TypeError("name must be a string");
24
+ if (body.metadata != null &&
25
+ (typeof body.metadata !== "object" ||
26
+ Array.isArray(body.metadata) ||
27
+ Object.values(body.metadata).some((value) => typeof value !== "string")))
28
+ throw new TypeError("metadata must contain string values");
29
+ // Keep the entire public payload durable, including arbitrary names and the
30
+ // official 1000-character commit message. Native labels have 256-char values.
31
+ const payload = JSON.stringify({
32
+ name: body.name ?? null,
33
+ metadata: body.metadata ?? {},
34
+ commit_message: body.commit_message ?? null,
35
+ });
36
+ const chunks = [];
37
+ let chunk = "";
38
+ for (const character of payload) {
39
+ if (chunk.length + character.length > 256) {
40
+ chunks.push(chunk);
41
+ chunk = "";
42
+ }
43
+ chunk += character;
44
+ }
45
+ if (chunk)
46
+ chunks.push(chunk);
47
+ if (chunks.length > 31)
48
+ throw new CompatibilityError("Runloop", "snapshot metadata exceeding Runtime's 31 label chunks");
49
+ const labels = {
50
+ "compat.provider": "runloop",
51
+ ...Object.fromEntries(chunks.map((chunk, index) => [`${PAYLOAD}${index.toString().padStart(2, "0")}`, chunk])),
52
+ };
53
+ // Match the spine's JSONB text bound, including its spaces between entries.
54
+ // Count UTF-8 bytes and never split surrogate pairs into invalid JSON strings.
55
+ const stored = `{${Object.entries(labels)
56
+ .map(([key, value]) => `${JSON.stringify(key)}: ${JSON.stringify(value)}`)
57
+ .join(", ")}}`;
58
+ if (new TextEncoder().encode(stored).length > 4096)
59
+ throw new CompatibilityError("Runloop", "snapshot metadata exceeding Runtime's 4096-byte label bound");
60
+ return {
61
+ mode: "disk",
62
+ labels,
63
+ };
64
+ }
65
+ function labelsOf(snapshot) {
66
+ const value = snapshot.labels;
67
+ return value && typeof value === "object"
68
+ ? Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === "string"))
69
+ : {};
70
+ }
71
+ export function snapshotView(snapshot) {
72
+ if (snapshot.mode !== "disk")
73
+ throw new CompatibilityError("Runloop", "using a memory snapshot as a disk snapshot");
74
+ const labels = labelsOf(snapshot);
75
+ const packed = Object.keys(labels)
76
+ .filter((key) => key.startsWith(PAYLOAD))
77
+ .sort()
78
+ .map((key) => labels[key])
79
+ .join("");
80
+ const body = packed
81
+ ? JSON.parse(packed)
82
+ : {
83
+ name: snapshot.name,
84
+ metadata: Object.fromEntries(Object.entries(labels).filter(([key]) => !key.startsWith("compat."))),
85
+ };
86
+ return {
87
+ id: snapshot.id,
88
+ create_time_ms: Date.parse(snapshot.createdAt),
89
+ metadata: body.metadata ?? {},
90
+ source_devbox_id: snapshot.sourceSandboxId,
91
+ commit_message: body.commit_message ?? null,
92
+ name: body.name ?? null,
93
+ size_bytes: snapshot.meteredBytes,
94
+ source_blueprint_id: typeof snapshot.sourceImageId === "string" ? snapshot.sourceImageId : null,
95
+ };
96
+ }
97
+ export class DiskSnapshots {
98
+ runtime;
99
+ constructor(runtime) {
100
+ this.runtime = runtime;
101
+ }
102
+ async update(id, body = {}, options = {}) {
103
+ if (requestOptions(body))
104
+ return this.update(id, {}, body);
105
+ snapshotOptions(body); // Reject invalid input before any request.
106
+ for (let attempt = 0;; attempt++) {
107
+ const current = await this.runtime.snapshots.get(id, nativeOptions(options));
108
+ const previous = snapshotView(current);
109
+ const merged = {
110
+ name: body.name === undefined ? previous.name : body.name,
111
+ metadata: body.metadata === undefined ? previous.metadata : body.metadata,
112
+ commit_message: body.commit_message === undefined ? previous.commit_message : body.commit_message,
113
+ };
114
+ const labels = snapshotOptions(merged).labels;
115
+ try {
116
+ return snapshotView(await this.runtime.snapshots.update(id, { labels, ifLabels: labelsOf(current) }, nativeOptions(options)));
117
+ }
118
+ catch (error) {
119
+ if (!(error instanceof RuntimeError) ||
120
+ error.code !== "snapshot_metadata_changed" ||
121
+ attempt >= 2)
122
+ throw error;
123
+ }
124
+ }
125
+ }
126
+ async awaitCompleted(id, options = {}) {
127
+ const timeoutMs = options.longPoll?.timeoutMs ?? options.polling?.timeoutMs;
128
+ const { maxAttempts, initialDelayMs = 0, pollingIntervalMs = 1000 } = options.polling ?? {};
129
+ if (options.signal?.aborted)
130
+ throw new LongPollAbortError("Polling aborted", undefined);
131
+ for (const [name, value] of Object.entries({ initialDelayMs, pollingIntervalMs, maxAttempts }))
132
+ if (value !== undefined && (!Number.isFinite(value) || value < 0))
133
+ throw new Error(`${name} must be non-negative`);
134
+ if (timeoutMs !== undefined && (!Number.isFinite(timeoutMs) || timeoutMs <= 0))
135
+ throw new Error("timeoutMs must be positive");
136
+ const controller = new AbortController();
137
+ const timer = timeoutMs === undefined ? undefined : setTimeout(() => controller.abort(), timeoutMs);
138
+ const signal = options.signal
139
+ ? AbortSignal.any([controller.signal, options.signal])
140
+ : controller.signal;
141
+ let lastResult;
142
+ const delay = (ms) => new Promise((resolve, reject) => {
143
+ signal.throwIfAborted();
144
+ const onAbort = () => {
145
+ clearTimeout(wait);
146
+ reject(signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason)));
147
+ };
148
+ const wait = setTimeout(() => {
149
+ signal.removeEventListener("abort", onAbort);
150
+ resolve();
151
+ }, ms);
152
+ signal.addEventListener("abort", onAbort, { once: true });
153
+ });
154
+ try {
155
+ let attempts = 0;
156
+ while (true) {
157
+ lastResult = await this.queryStatus(id, { ...options, signal });
158
+ if (lastResult.status === "error")
159
+ throw new Error(`Snapshot ${id} failed: ${lastResult.error_message || "Unknown error"}`);
160
+ if (lastResult.status === "complete" || (maxAttempts === 0 && attempts === 0))
161
+ return lastResult;
162
+ if (maxAttempts !== undefined && attempts === maxAttempts)
163
+ throw new MaxAttemptsExceededError(`Polling exceeded maximum attempts (${maxAttempts})`, lastResult);
164
+ await delay(attempts++ === 0 ? initialDelayMs : pollingIntervalMs);
165
+ }
166
+ }
167
+ catch (error) {
168
+ if (options.signal?.aborted)
169
+ throw new LongPollAbortError("Polling aborted", lastResult);
170
+ if (controller.signal.aborted)
171
+ throw new PollingTimeoutError(`Polling timed out after ${timeoutMs}ms`, lastResult);
172
+ throw error;
173
+ }
174
+ finally {
175
+ clearTimeout(timer);
176
+ }
177
+ }
178
+ async queryStatus(id, options = {}) {
179
+ const snapshot = await this.runtime.snapshots.get(id, nativeOptions(options));
180
+ const view = snapshotView(snapshot);
181
+ return {
182
+ status: snapshot.state === "ready"
183
+ ? "complete"
184
+ : snapshot.state === "failed"
185
+ ? "error"
186
+ : snapshot.state === "deleted" || snapshot.state === "deleting"
187
+ ? "deleted"
188
+ : "in_progress",
189
+ error_message: snapshot.error,
190
+ snapshot: view,
191
+ };
192
+ }
193
+ async delete(id, options = {}) {
194
+ snapshotView(await this.runtime.snapshots.get(id, nativeOptions(options)));
195
+ await this.runtime.snapshots.delete(id, nativeOptions(options));
196
+ return {};
197
+ }
198
+ list(query = {}, options = {}) {
199
+ if (requestOptions(query))
200
+ return this.list({}, query);
201
+ return new PagePromise(this.page(query, options));
202
+ }
203
+ async page(query, options) {
204
+ options.signal?.throwIfAborted();
205
+ const filters = Object.entries(query).filter(([key]) => /^metadata\[[^\]]+\](\[in\])?$/.test(key));
206
+ only("Runloop snapshot list", query, [
207
+ "starting_after",
208
+ "limit",
209
+ "devbox_id",
210
+ "include_total_count",
211
+ "source_blueprint_id",
212
+ ...filters.map(([key]) => key),
213
+ ]);
214
+ if (query.limit !== undefined && (!Number.isInteger(query.limit) || query.limit <= 0))
215
+ throw new TypeError("limit must be positive");
216
+ const values = [];
217
+ const page = await this.runtime.snapshots.list({ sandboxId: query.devbox_id }, nativeOptions(options));
218
+ for await (const snapshot of page) {
219
+ options.signal?.throwIfAborted();
220
+ if (snapshot.mode !== "disk" || labelsOf(snapshot)["compat.provider"] !== "runloop")
221
+ continue;
222
+ const value = snapshotView(snapshot);
223
+ if (query.source_blueprint_id && value.source_blueprint_id !== query.source_blueprint_id)
224
+ continue;
225
+ if (!filters.every(([key, expected]) => {
226
+ const name = key.slice(9, key.indexOf("]"));
227
+ return key.endsWith("[in]")
228
+ ? typeof expected === "string" &&
229
+ expected.split(",").includes(value.metadata[name] ?? "")
230
+ : value.metadata[name] === expected;
231
+ }))
232
+ continue;
233
+ values.push(value);
234
+ }
235
+ const total = values.length;
236
+ const offset = query.starting_after
237
+ ? values.findIndex((value) => value.id === query.starting_after) + 1
238
+ : 0;
239
+ if (query.starting_after && offset === 0)
240
+ throw new TypeError("starting_after does not identify a matching snapshot");
241
+ const data = values.slice(offset, offset + (query.limit ?? 20));
242
+ return new DevboxSnapshotViewsDiskSnapshotsCursorIDPage(data, offset + data.length < total, query.include_total_count === false ? 0 : total, (after) => this.page({ ...query, starting_after: after }, options));
243
+ }
244
+ }
package/dist/sandbox.d.ts CHANGED
@@ -1,10 +1,13 @@
1
1
  import { Page } from "./page.js";
2
2
  import { type FileEvent, type WatchHandle, type WatchOptions } from "./products/watch.js";
3
3
  import { Transport, type RequestOptions } from "./transport.js";
4
- import type { CommandResult, ExecOptions, FileEntry, OutputEvent, KeepAliveOptions, ProcessInfo, SandboxInfo, SandboxSettings } from "./types.js";
4
+ import type { CommandResult, DeletedSandbox, ExecOptions, FileEntry, OutputEvent, BinaryOutputEvent, KeepAliveOptions, ProcessInfo, SandboxInfo, SandboxSettings } from "./types.js";
5
5
  import { type SandboxExtensions } from "./products/index.js";
6
6
  import type { Snapshot, SnapshotOptions } from "./snapshots.js";
7
7
  import { Tunnel, type PortForward } from "./tunnel.js";
8
+ /** DELETE /v1/sandboxes/{id}, for `sandbox.delete()` and
9
+ * `runtime.sandboxes.delete(id)`. */
10
+ export declare function deleteSandbox(t: Transport, id: string, options?: RequestOptions): Promise<DeletedSandbox>;
8
11
  /** A running (or stopped) sandbox. Every method is one API call or a short
9
12
  * sequence of them; `await using sbx = await Sandbox.create()` stops it for
10
13
  * you at the end of the block. */
@@ -63,6 +66,7 @@ export declare class Sandbox implements AsyncDisposable {
63
66
  cols?: number;
64
67
  rows?: number;
65
68
  };
69
+ outputEncoding?: "utf8" | "base64";
66
70
  }): Promise<Process>;
67
71
  /** An interactive terminal over a WebSocket. Bytes you write are typed;
68
72
  * onData receives what the terminal prints. */
@@ -93,6 +97,11 @@ export declare class Sandbox implements AsyncDisposable {
93
97
  pause(options?: RequestOptions & {
94
98
  wait?: boolean;
95
99
  }): Promise<this>;
100
+ /** Deletes it for good: stops it if it runs or is paused, deletes its disk
101
+ * and paused memory, revokes its previews and ports, and removes it from
102
+ * lists. Its snapshots, usage and audit entries stay. Deleting it again
103
+ * answers the same. */
104
+ delete(options?: RequestOptions): Promise<DeletedSandbox>;
96
105
  /** Carries on a paused sandbox; `timeoutSeconds` is its new lease. */
97
106
  wake(options?: RequestOptions & {
98
107
  wait?: boolean;
@@ -104,6 +113,17 @@ export declare class Sandbox implements AsyncDisposable {
104
113
  * fields left out stay as they are. `persistent: true` keeps it running
105
114
  * while credit lasts (paid only). */
106
115
  update(settings: SandboxSettings, options?: RequestOptions): Promise<this>;
116
+ /** Moves it to another image (id, name, name:tag or name@version), keeping
117
+ * its id, /workspace (its home: dotfiles, pip --user and npm -g installs),
118
+ * volumes, environment, name and previews. Its processes restart, and
119
+ * everything else on its old disk (sudo installs, apt packages, /etc) is
120
+ * lost, which `keep: "workspace"` says you know; snapshot it first to keep
121
+ * everything. A running sandbox is paused first. A switch that fails is
122
+ * undone (`switch_undone`), the sandbox on its old image with nothing lost.
123
+ * Charged as a wake. */
124
+ switchImage(image: string, options: RequestOptions & {
125
+ keep: "workspace";
126
+ }): Promise<this>;
107
127
  /** Keeps a running sandbox's lease ahead of now, in the background, until
108
128
  * stop() or the returned function ends it: every `everySeconds` (60) it
109
129
  * extends the lease so that `marginSeconds` (600) remain, never more than
@@ -211,6 +231,12 @@ export declare class Process {
211
231
  cursor?: number;
212
232
  signal?: AbortSignal;
213
233
  }): AsyncGenerator<OutputEvent>;
234
+ /** Lossless output for a process spawned with outputEncoding: "base64".
235
+ * Refuses text-only output rather than inventing bytes that were discarded. */
236
+ outputBytes(options?: {
237
+ cursor?: number;
238
+ signal?: AbortSignal;
239
+ }): AsyncGenerator<BinaryOutputEvent>;
214
240
  /** Waits for the process to end and returns its result. */
215
241
  wait(options?: {
216
242
  signal?: AbortSignal;
@@ -262,6 +288,9 @@ export declare class Files {
262
288
  lostBytes: number;
263
289
  ended: boolean;
264
290
  }>;
291
+ webhook: (path: string, input?: import("./products/watch.js").WebhookWatchOptions) => Promise<import("./products/watch.js").WatchInfo & {
292
+ cursor: number;
293
+ }>;
265
294
  stop: (id: string, options?: RequestOptions) => Promise<{
266
295
  stopped: boolean;
267
296
  }>;
@@ -296,29 +325,31 @@ export declare class Files {
296
325
  glob?: string;
297
326
  hidden?: boolean;
298
327
  limit?: number;
299
- }): Promise<FileEntry[]>;
328
+ } & RequestOptions): Promise<FileEntry[]>;
300
329
  glob(pattern: string, root?: string): Promise<FileEntry[]>;
301
- stat(path: string): Promise<(FileEntry & {
330
+ stat(path: string, options?: RequestOptions): Promise<(FileEntry & {
302
331
  exists: true;
303
332
  }) | {
304
333
  exists: false;
305
334
  path: string;
306
335
  }>;
307
- exists(path: string): Promise<boolean>;
336
+ exists(path: string, options?: RequestOptions): Promise<boolean>;
308
337
  mkdir(path: string, options?: {
309
338
  parents?: boolean;
310
- }): Promise<void>;
339
+ } & RequestOptions): Promise<void>;
311
340
  remove(path: string, options?: {
312
341
  recursive?: boolean;
313
- }): Promise<boolean>;
342
+ } & RequestOptions): Promise<boolean>;
314
343
  rename(from: string, to: string, options?: {
315
344
  overwrite?: boolean;
316
- }): Promise<void>;
345
+ } & RequestOptions): Promise<void>;
317
346
  /** Copies a local file or directory into the sandbox. A directory travels as
318
- * one gzipped tar and is unpacked in place. */
347
+ * one gzipped tar to the API's folder routes, and the sandbox's own `tar`
348
+ * unpacks it as it arrives, making the folder. */
319
349
  upload(localPath: string, remotePath: string): Promise<void>;
320
350
  /** Copies a file or directory out of the sandbox. A file streams to disk,
321
- * any size, checked against its length. */
351
+ * any size, checked against its length; a directory comes as one tar from
352
+ * the API's folder route. */
322
353
  download(remotePath: string, localPath: string): Promise<void>;
323
354
  }
324
355
  export declare function sandboxPage(t: Transport, body: {