withruntime 0.11.2 → 0.12.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 (164) hide show
  1. package/BLAXEL.md +2 -2
  2. package/COMPATIBILITY.md +74 -9
  3. package/DAYTONA.md +33 -5
  4. package/E2B.md +85 -24
  5. package/README.md +9 -8
  6. package/README.src.md +8 -7
  7. package/VERCEL.md +43 -5
  8. package/compatibility-lock.json +58 -49
  9. package/dist/billing-cli.d.ts +1 -1
  10. package/dist/billing-cli.js +77 -1
  11. package/dist/blaxel/errors.d.ts +14 -0
  12. package/dist/blaxel/errors.js +29 -0
  13. package/dist/blaxel/index.d.ts +2 -2
  14. package/dist/blaxel/index.js +4 -4
  15. package/dist/blaxel/interpreter.d.ts +2 -5
  16. package/dist/blaxel/sandbox.d.ts +14 -5
  17. package/dist/blaxel/sandbox.js +63 -7
  18. package/dist/blaxel/snapshot.d.ts +4 -0
  19. package/dist/blaxel/snapshot.js +4 -0
  20. package/dist/cli/logs.d.ts +21 -0
  21. package/dist/cli/logs.js +188 -0
  22. package/dist/cli/logs.test.d.ts +1 -0
  23. package/dist/cli/logs.test.js +272 -0
  24. package/dist/cli-extras.d.ts +3 -3
  25. package/dist/cli-extras.js +17 -12
  26. package/dist/cli-words.d.ts +10 -1
  27. package/dist/cli-words.js +18 -0
  28. package/dist/cli.d.ts +5 -1
  29. package/dist/cli.js +405 -204
  30. package/dist/client.d.ts +2 -4
  31. package/dist/client.js +27 -11
  32. package/dist/cloudflare/errors.d.ts +143 -0
  33. package/dist/cloudflare/errors.js +167 -0
  34. package/dist/cloudflare/index.d.ts +175 -6
  35. package/dist/cloudflare/index.js +416 -26
  36. package/dist/cloudflare/interpreter.d.ts +119 -0
  37. package/dist/cloudflare/interpreter.js +223 -0
  38. package/dist/cloudflare/streams.d.ts +38 -0
  39. package/dist/cloudflare/streams.js +145 -0
  40. package/dist/cloudflare/watch.d.ts +9 -0
  41. package/dist/cloudflare/watch.js +24 -0
  42. package/dist/codesandbox/index.d.ts +216 -28
  43. package/dist/codesandbox/index.js +478 -78
  44. package/dist/compat/core.d.ts +18 -0
  45. package/dist/compat/core.js +29 -0
  46. package/dist/compat/files.d.ts +1 -0
  47. package/dist/compat/files.js +25 -5
  48. package/dist/daytona/computer-use.d.ts +13 -4
  49. package/dist/daytona/computer-use.js +66 -8
  50. package/dist/daytona/daytona.d.ts +6 -0
  51. package/dist/daytona/daytona.js +30 -3
  52. package/dist/daytona/errors.d.ts +11 -0
  53. package/dist/daytona/errors.js +18 -0
  54. package/dist/daytona/filesystem.js +2 -1
  55. package/dist/daytona/image.d.ts +13 -9
  56. package/dist/daytona/image.js +18 -12
  57. package/dist/daytona/index.d.ts +1 -1
  58. package/dist/daytona/sandbox.d.ts +8 -2
  59. package/dist/daytona/sandbox.js +16 -2
  60. package/dist/domains/index.d.ts +9 -0
  61. package/dist/domains/index.js +10 -0
  62. package/dist/e2b/commands.d.ts +1 -1
  63. package/dist/e2b/commands.js +6 -7
  64. package/dist/e2b/errors.d.ts +17 -0
  65. package/dist/e2b/errors.js +40 -1
  66. package/dist/e2b/index.d.ts +2 -2
  67. package/dist/e2b/index.js +1 -1
  68. package/dist/e2b/sandbox.d.ts +27 -21
  69. package/dist/e2b/sandbox.js +34 -8
  70. package/dist/e2b/unsupported.js +1 -1
  71. package/dist/errors.d.ts +10 -1
  72. package/dist/errors.js +16 -4
  73. package/dist/freestyle/index.d.ts +285 -37
  74. package/dist/freestyle/index.js +656 -144
  75. package/dist/identity.d.ts +3 -0
  76. package/dist/identity.js +27 -0
  77. package/dist/images/index.d.ts +9 -0
  78. package/dist/images/index.js +10 -0
  79. package/dist/index.d.ts +1 -0
  80. package/dist/ips/index.d.ts +9 -0
  81. package/dist/ips/index.js +10 -0
  82. package/dist/jobs/index.d.ts +9 -0
  83. package/dist/jobs/index.js +10 -0
  84. package/dist/keys.js +2 -0
  85. package/dist/login.js +2 -0
  86. package/dist/machine.d.ts +12 -3
  87. package/dist/machine.js +20 -3
  88. package/dist/mcp.js +1 -1
  89. package/dist/modal/index.d.ts +339 -61
  90. package/dist/modal/index.js +781 -121
  91. package/dist/network-cli.d.ts +3 -1
  92. package/dist/network-cli.js +78 -36
  93. package/dist/networks/index.d.ts +11 -0
  94. package/dist/networks/index.js +10 -0
  95. package/dist/ports/index.d.ts +9 -0
  96. package/dist/ports/index.js +10 -0
  97. package/dist/product-names.d.ts +52 -0
  98. package/dist/product-names.js +164 -0
  99. package/dist/product-renames.d.ts +103 -0
  100. package/dist/product-renames.js +189 -0
  101. package/dist/products/apps.d.ts +176 -0
  102. package/dist/products/apps.js +165 -0
  103. package/dist/products/billing.d.ts +41 -0
  104. package/dist/products/billing.js +13 -0
  105. package/dist/products/browser.d.ts +10 -0
  106. package/dist/products/browsers.d.ts +71 -0
  107. package/dist/products/browsers.js +102 -0
  108. package/dist/products/builds.d.ts +126 -0
  109. package/dist/products/builds.js +221 -0
  110. package/dist/products/index.d.ts +3 -16
  111. package/dist/products/index.js +3 -14
  112. package/dist/products/jobs.d.ts +17 -0
  113. package/dist/products/jobs.js +27 -10
  114. package/dist/products/logs.d.ts +245 -0
  115. package/dist/products/logs.js +274 -0
  116. package/dist/products/logs.test.d.ts +1 -0
  117. package/dist/products/logs.test.js +343 -0
  118. package/dist/products/network-products.d.ts +38 -7
  119. package/dist/products/network-products.js +31 -18
  120. package/dist/products/observability.d.ts +3 -2
  121. package/dist/products/private-network.js +1 -1
  122. package/dist/products/queues.d.ts +149 -0
  123. package/dist/products/queues.js +213 -0
  124. package/dist/products/referrals.d.ts +21 -7
  125. package/dist/products/referrals.js +3 -3
  126. package/dist/products/secrets.d.ts +7 -3
  127. package/dist/products/secrets.js +39 -18
  128. package/dist/products/signed-requests.d.ts +21 -0
  129. package/dist/products/signed-requests.js +11 -0
  130. package/dist/products/streams.d.ts +73 -0
  131. package/dist/products/streams.js +17 -0
  132. package/dist/products/switching.d.ts +35 -7
  133. package/dist/products/switching.js +4 -3
  134. package/dist/products/volumes.d.ts +3 -0
  135. package/dist/record-cli.d.ts +23 -0
  136. package/dist/record-cli.js +308 -0
  137. package/dist/renamed-products.d.ts +38 -0
  138. package/dist/renamed-products.js +34 -0
  139. package/dist/runloop/index.d.ts +9 -2
  140. package/dist/runloop/index.js +29 -8
  141. package/dist/runloop/sdk.d.ts +131 -0
  142. package/dist/runloop/sdk.js +167 -0
  143. package/dist/sandbox.d.ts +3 -2
  144. package/dist/sandbox.js +14 -9
  145. package/dist/sandboxes/index.d.ts +11 -0
  146. package/dist/sandboxes/index.js +12 -0
  147. package/dist/snapshots/index.d.ts +9 -0
  148. package/dist/snapshots/index.js +11 -0
  149. package/dist/snapshots.d.ts +7 -4
  150. package/dist/sprites/index.d.ts +343 -31
  151. package/dist/sprites/index.js +866 -84
  152. package/dist/ssh.d.ts +1 -1
  153. package/dist/ssh.js +11 -11
  154. package/dist/terminal.d.ts +24 -0
  155. package/dist/terminal.js +32 -0
  156. package/dist/transport.d.ts +15 -1
  157. package/dist/transport.js +56 -3
  158. package/dist/tunnel.js +1 -1
  159. package/dist/types.d.ts +63 -0
  160. package/dist/vercel/sandbox.js +5 -3
  161. package/dist/vercel/snapshot.js +10 -2
  162. package/dist/volumes/index.d.ts +9 -0
  163. package/dist/volumes/index.js +10 -0
  164. package/package.json +48 -2
@@ -1,8 +1,16 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { Runtime } from "../client.js";
3
3
  import { NotFoundError } from "../errors.js";
4
- import { client, closeShell, CompatibilityError, destroy, environment, execOptions, only, pathAt, saveEnvironment, shellCommand, shellRoot, } from "../compat/core.js";
4
+ import { timeoutSignal } from "../transport.js";
5
+ import { quote, client, closeShell, CompatibilityError, refuse, destroy, environment, execOptions, only, pathAt, saveEnvironment, shellCommand, shellRoot, } from "../compat/core.js";
6
+ import { BucketUnmountError, InvalidMountConfigError, ProcessExitedBeforeReadyError, ProcessReadyTimeoutError, S3FSMountError, response, } from "./errors.js";
7
+ import { CodeInterpreter, hasEnv, RUN_ENV, } from "./interpreter.js";
8
+ import { sseStream } from "./streams.js";
9
+ import { CHANGES, watchFilters } from "./watch.js";
5
10
  export { CompatibilityError };
11
+ export * from "./errors.js";
12
+ export { CodeInterpreter } from "./interpreter.js";
13
+ export { collectFile, isExecResult, isProcess, isProcessStatus, parseSSEStream, streamFile, } from "./streams.js";
6
14
  export class SandboxSecurityError extends Error {
7
15
  code;
8
16
  constructor(message, code) {
@@ -55,12 +63,33 @@ export class Sandbox {
55
63
  pending;
56
64
  existing;
57
65
  id;
66
+ /** The port Cloudflare's container serves on; Runtime previews any port. */
67
+ defaultPort = 3000;
68
+ /** What setEnvVars gave this handle; the sandbox keeps them for every command. */
69
+ envVars = {};
70
+ #interpreter = new CodeInterpreter(this);
58
71
  constructor(runtime, id, options = {}) {
59
72
  this.runtime = runtime;
60
73
  this.options = options;
61
74
  validateSandboxConfiguration(options);
62
75
  this.id = id;
63
76
  }
77
+ get sleepAfter() {
78
+ return this.options.sleepAfter ?? "10m";
79
+ }
80
+ setSleepAfter(sleepAfter) {
81
+ return this.configure({ sleepAfter });
82
+ }
83
+ setKeepAlive(keepAlive) {
84
+ return this.configure({ keepAlive });
85
+ }
86
+ /** Replaces the sandbox's labels, as Cloudflare's does. */
87
+ async setLabels(labels) {
88
+ for (const [key, value] of Object.entries(labels))
89
+ if (typeof value !== "string")
90
+ throw new TypeError(`Label ${key} must be a string`);
91
+ await (await this.native()).update({ labels: { ...labels, "compat.provider": "cloudflare" } });
92
+ }
64
93
  async configure(options) {
65
94
  only("Cloudflare configure", options, ["sleepAfter", "keepAlive"]);
66
95
  validateSandboxConfiguration(options);
@@ -131,14 +160,42 @@ export class Sandbox {
131
160
  session(id = `sandbox-${this.id}`, options = {}) {
132
161
  return new ExecutionSession(this, id, options);
133
162
  }
163
+ /** A session by id: its saved settings, or a fresh session of that id, as
164
+ * Cloudflare starts one again after its shell ended. */
165
+ async sessionFor(id) {
166
+ if (id === undefined || id === `sandbox-${this.id}`)
167
+ return this.session();
168
+ try {
169
+ return await this.getSession(id);
170
+ }
171
+ catch (error) {
172
+ if (error instanceof NotFoundError)
173
+ return this.session(id);
174
+ throw error;
175
+ }
176
+ }
134
177
  exec(command, options) {
135
178
  return this.session().exec(command, options);
136
179
  }
137
- execStream(command, options) {
138
- return this.session().execStream(command, options);
180
+ async execWithSessionToken(command, sessionId, options) {
181
+ return (await this.sessionFor(sessionId)).exec(command, options);
139
182
  }
140
- startProcess(command, options = {}) {
141
- return this.session(options.sessionId).startProcess(command, options);
183
+ async execStream(command, options = {}) {
184
+ const { sessionId, bufferSize, ...rest } = options;
185
+ if (bufferSize !== undefined && (!Number.isSafeInteger(bufferSize) || bufferSize <= 0))
186
+ throw new TypeError("bufferSize must be a positive integer");
187
+ return (await this.sessionFor(sessionId)).execStream(command, rest);
188
+ }
189
+ execStreamWithSessionToken(command, sessionId, options = {}) {
190
+ return this.execStream(command, { ...options, sessionId });
191
+ }
192
+ async startProcess(command, options = {}) {
193
+ if (options.processId !== undefined)
194
+ throw new CompatibilityError("Cloudflare", "processId; Runtime names each process itself");
195
+ if (options.autoCleanup)
196
+ throw new CompatibilityError("Cloudflare", "autoCleanup; Runtime keeps a finished process's record and output until the sandbox ends");
197
+ const { autoCleanup: _kept, ...rest } = options;
198
+ return this.session(options.sessionId).startProcess(command, rest);
142
199
  }
143
200
  async createSession(options = {}) {
144
201
  only("Cloudflare session", options, [
@@ -175,32 +232,74 @@ export class Sandbox {
175
232
  async setEnvVars(env) {
176
233
  const s = await this.native();
177
234
  await saveEnvironment(s, { ...(await environment(s)), ...cleanEnv(env) });
235
+ this.envVars = { ...this.envVars, ...cleanEnv(env) };
178
236
  }
179
- writeFile(path, content, options) {
180
- return this.session().writeFile(path, content, options);
237
+ /** Stops the sandbox; its files stay, and the next call starts it again. */
238
+ async stop(signal) {
239
+ if (signal !== undefined)
240
+ throw new CompatibilityError("Cloudflare", "a stop signal; Runtime stops every process");
241
+ await (await this.native()).stop();
242
+ }
243
+ async writeFile(path, content, options = {}) {
244
+ const { sessionId, ...rest } = options;
245
+ return (await this.sessionFor(sessionId)).writeFile(path, content, rest);
181
246
  }
182
- readFile(path, options) {
183
- return options?.encoding === "none"
184
- ? this.session().readFile(path, { encoding: "none" })
185
- : this.session().readFile(path, options);
247
+ async readFile(path, options = {}) {
248
+ const { sessionId, encoding } = options;
249
+ const session = await this.sessionFor(sessionId);
250
+ return encoding === "none"
251
+ ? session.readFile(path, { encoding: "none" })
252
+ : session.readFile(path, { encoding });
253
+ }
254
+ /** The file as Cloudflare streams it: `data:` lines of a metadata event,
255
+ * chunks (base64 for a binary file) and a complete event; read it with
256
+ * streamFile() or collectFile(). */
257
+ async readFileStream(path, options = {}) {
258
+ const read = await this.readFile(path, { encoding: "none", sessionId: options.sessionId });
259
+ const bytes = new Uint8Array(await new Response(read.content).arrayBuffer());
260
+ const text = new TextDecoder().decode(bytes);
261
+ const isBinary = text.includes("\0") || text.includes("\ufffd");
262
+ return sseStream(async (send) => {
263
+ send({
264
+ type: "metadata",
265
+ mimeType: isBinary ? "application/octet-stream" : "text/plain",
266
+ size: bytes.length,
267
+ isBinary,
268
+ encoding: isBinary ? "base64" : "utf-8",
269
+ });
270
+ const CHUNK = 65_536;
271
+ if (isBinary)
272
+ for (let i = 0; i < bytes.length; i += CHUNK) {
273
+ let raw = "";
274
+ for (const byte of bytes.subarray(i, i + CHUNK))
275
+ raw += String.fromCharCode(byte);
276
+ send({ type: "chunk", data: btoa(raw) });
277
+ }
278
+ else
279
+ for (let i = 0; i < text.length; i += CHUNK)
280
+ send({ type: "chunk", data: text.slice(i, i + CHUNK) });
281
+ send({ type: "complete", bytesRead: bytes.length });
282
+ });
186
283
  }
187
- mkdir(path, options) {
188
- return this.session().mkdir(path, options);
284
+ async mkdir(path, options = {}) {
285
+ const { sessionId, ...rest } = options;
286
+ return (await this.sessionFor(sessionId)).mkdir(path, rest);
189
287
  }
190
- deleteFile(path) {
191
- return this.session().deleteFile(path);
288
+ async deleteFile(path, sessionId) {
289
+ return (await this.sessionFor(sessionId)).deleteFile(path);
192
290
  }
193
- renameFile(from, to) {
194
- return this.session().renameFile(from, to);
291
+ async renameFile(from, to, sessionId) {
292
+ return (await this.sessionFor(sessionId)).renameFile(from, to);
195
293
  }
196
- moveFile(from, to) {
197
- return this.renameFile(from, to);
294
+ moveFile(from, to, sessionId) {
295
+ return this.renameFile(from, to, sessionId);
198
296
  }
199
- listFiles(path, options) {
200
- return this.session().listFiles(path, options);
297
+ async listFiles(path, options = {}) {
298
+ const { sessionId, ...rest } = options;
299
+ return (await this.sessionFor(sessionId)).listFiles(path, rest);
201
300
  }
202
- exists(path) {
203
- return this.session().exists(path);
301
+ async exists(path, sessionId) {
302
+ return (await this.sessionFor(sessionId)).exists(path);
204
303
  }
205
304
  async listProcesses() {
206
305
  const s = await this.native();
@@ -262,6 +361,270 @@ export class Sandbox {
262
361
  async unexposePort(port) {
263
362
  await (await this.native()).previews.delete(port);
264
363
  }
364
+ /** Runtime's previews have their own addresses: hostname is not used to build them. */
365
+ async getExposedPorts(_hostname) {
366
+ return (await (await this.native()).previews.list()).map((p) => ({
367
+ url: p.urlWithToken ?? p.url,
368
+ port: p.port,
369
+ status: "active",
370
+ }));
371
+ }
372
+ async isPortExposed(port) {
373
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
374
+ return false;
375
+ return (await (await this.native()).previews.list()).some((p) => p.port === port);
376
+ }
377
+ /** Cloudflare's returns 0 too: a process's record outlives it on both. */
378
+ async cleanupCompletedProcesses(_sessionId) {
379
+ return 0;
380
+ }
381
+ /** The process's output as `data:` lines of stdout, stderr and exit events. */
382
+ async streamProcessLogs(processId, options = {}) {
383
+ only("Cloudflare streamProcessLogs", options, ["signal"]);
384
+ options.signal?.throwIfAborted();
385
+ const p = await (await this.native()).processes.get(processId);
386
+ return sseStream(async (send, cancelled) => {
387
+ const signal = options.signal ? AbortSignal.any([options.signal, cancelled]) : cancelled;
388
+ for await (const e of p.output({ signal })) {
389
+ const at = new Date().toISOString();
390
+ if (e.type === "stdout" || e.type === "stderr")
391
+ send({ type: e.type, timestamp: at, data: e.data, processId });
392
+ else if (e.type === "exit")
393
+ send({ type: "exit", timestamp: at, data: "", processId, exitCode: e.exitCode });
394
+ else if (e.type === "truncated")
395
+ send({ type: "error", timestamp: at, data: "Process output was truncated", processId });
396
+ }
397
+ });
398
+ }
399
+ /** Clones with git in the sandbox, as Cloudflare's container does. */
400
+ async gitCheckout(repoUrl, options = {}) {
401
+ only("Cloudflare gitCheckout", options, [
402
+ "branch",
403
+ "targetDir",
404
+ "sessionId",
405
+ "depth",
406
+ "cloneTimeoutMs",
407
+ ]);
408
+ const timeout = options.cloneTimeoutMs ?? 600_000;
409
+ if (options.depth !== undefined && (!Number.isInteger(options.depth) || options.depth <= 0))
410
+ throw new Error(`Invalid depth value: ${options.depth}. Must be a positive integer (e.g., 1, 5, 10).`);
411
+ if (!Number.isInteger(timeout) || timeout <= 0)
412
+ throw new Error(`Invalid timeout value: ${timeout}. Must be a positive integer number of milliseconds.`);
413
+ const name = repoUrl
414
+ .replace(/\/+$/, "")
415
+ .split(/[/:]/)
416
+ .pop()
417
+ ?.replace(/\.git$/, "") || "repo";
418
+ const targetDir = options.targetDir ?? `/workspace/${name}`;
419
+ const session = await this.sessionFor(options.sessionId);
420
+ const args = [
421
+ "git clone",
422
+ ...(options.branch ? ["--branch", quote(options.branch)] : []),
423
+ ...(options.depth ? ["--depth", String(options.depth)] : []),
424
+ "--",
425
+ quote(repoUrl),
426
+ quote(targetDir),
427
+ ];
428
+ const clone = await session.exec(`${args.join(" ")} && git -C ${quote(targetDir)} rev-parse --abbrev-ref HEAD`, { timeout });
429
+ if (!clone.success)
430
+ throw new Error(`Failed to clone ${repoUrl}: ${clone.stderr.trim() || clone.stdout.trim()}`);
431
+ return {
432
+ success: true,
433
+ repoUrl,
434
+ branch: options.branch ?? clone.stdout.trim().split("\n").pop(),
435
+ targetDir,
436
+ timestamp: new Date().toISOString(),
437
+ exitCode: clone.exitCode,
438
+ };
439
+ }
440
+ /** Changes under a directory as `data:` lines of Cloudflare's watch events,
441
+ * once the watch is established, until the stream is cancelled. */
442
+ async watch(path, options = {}) {
443
+ only("Cloudflare watch", options, ["recursive", "include", "exclude", "sessionId"]);
444
+ const filters = watchFilters(options);
445
+ const directory = await (await this.sessionFor(options.sessionId)).resolve(path);
446
+ const encode = new TextEncoder();
447
+ let stream;
448
+ let closed = false;
449
+ const send = (event) => {
450
+ if (!closed)
451
+ stream.enqueue(encode.encode(`data: ${JSON.stringify(event)}\n\n`));
452
+ };
453
+ const handle = await (await this.native()).files.watch(directory, (e) => {
454
+ const timestamp = new Date().toISOString();
455
+ const event = (eventType, at) => send({ type: "event", eventType, path: at, isDirectory: e.isDir, timestamp });
456
+ if (e.type === "rename") {
457
+ if (e.oldPath)
458
+ event("move_from", e.oldPath);
459
+ event("move_to", e.path);
460
+ }
461
+ else
462
+ event({ create: "create", write: "modify", remove: "delete", chmod: "attrib" }[e.type] ?? e.type, e.path);
463
+ }, {
464
+ recursive: options.recursive ?? true,
465
+ ...filters,
466
+ timeoutMs: 0,
467
+ onNotice: (n) => send({ type: "error", error: n.k === "overflow" ? "Events were lost" : `Watch ${n.k}` }),
468
+ onExit: (reason) => {
469
+ send({ type: "stopped", reason });
470
+ if (!closed) {
471
+ closed = true;
472
+ stream.close();
473
+ }
474
+ },
475
+ });
476
+ return new ReadableStream({
477
+ start(controller) {
478
+ stream = controller;
479
+ send({ type: "watching", path: directory, watchId: handle.id });
480
+ },
481
+ async cancel() {
482
+ closed = true;
483
+ await handle.stop();
484
+ },
485
+ });
486
+ }
487
+ /** Whether anything under a path changed since a version this returned:
488
+ * the version is a digest of every entry's path, size, time and mode, tied
489
+ * to this boot of the sandbox. */
490
+ async checkChanges(path, options = {}) {
491
+ only("Cloudflare checkChanges", options, [
492
+ "since",
493
+ "recursive",
494
+ "include",
495
+ "exclude",
496
+ "sessionId",
497
+ ]);
498
+ const filters = watchFilters(options);
499
+ const session = await this.sessionFor(options.sessionId);
500
+ const r = await (await this.native()).exec([
501
+ "python3",
502
+ "-c",
503
+ CHANGES,
504
+ await session.resolve(path),
505
+ options.recursive === false ? "0" : "1",
506
+ JSON.stringify(filters.include ?? []),
507
+ JSON.stringify(filters.exclude ?? []),
508
+ ]);
509
+ if (r.exitCode !== 0)
510
+ throw new Error(`checkChanges failed: ${r.stderr.trim()}`);
511
+ const version = r.stdout.trim();
512
+ const timestamp = new Date().toISOString();
513
+ if (options.since === undefined || options.since === version)
514
+ return {
515
+ success: true,
516
+ status: options.since === undefined ? "changed" : "unchanged",
517
+ version,
518
+ timestamp,
519
+ };
520
+ return options.since.split(".")[0] === version.split(".")[0]
521
+ ? { success: true, status: "changed", version, timestamp }
522
+ : { success: true, status: "resync", reason: "restarted", version, timestamp };
523
+ }
524
+ /** Mounts an S3, R2 or Google Cloud Storage bucket with Runtime's mounts. A
525
+ * private bucket needs a Runtime secret: store its keys with
526
+ * runtime.secrets.set and mount with sandbox.mounts.add({ secret }). */
527
+ async mountBucket(bucket, mountPath, options) {
528
+ only("Cloudflare mountBucket", options, [
529
+ "endpoint",
530
+ "provider",
531
+ "credentials",
532
+ "readOnly",
533
+ "s3fsOptions",
534
+ "prefix",
535
+ "credentialProxy",
536
+ "localBucket",
537
+ ]);
538
+ if (options.credentials || options.credentialProxy)
539
+ throw new CompatibilityError("Cloudflare", "credentials in mountBucket; store them with runtime.secrets.set and mount with sandbox.mounts.add({ secret })");
540
+ if (options.s3fsOptions?.length)
541
+ throw new CompatibilityError("Cloudflare", "s3fsOptions; Runtime mounts with its own client");
542
+ if (options.localBucket || options.endpoint === undefined)
543
+ throw new CompatibilityError("Cloudflare", "an R2 binding; give the bucket's endpoint, https://<account>.r2.cloudflarestorage.com");
544
+ if (!mountPath.startsWith("/"))
545
+ throw new InvalidMountConfigError(`Mount path must be absolute: ${mountPath}`);
546
+ let host;
547
+ try {
548
+ host = new URL(options.endpoint).hostname;
549
+ }
550
+ catch {
551
+ throw new InvalidMountConfigError(`Invalid endpoint URL: ${options.endpoint}`);
552
+ }
553
+ const r2 = /^([a-f0-9]{32})\.r2\.cloudflarestorage\.com$/.exec(host);
554
+ const provider = options.provider ?? (r2 ? "r2" : host === "storage.googleapis.com" ? "gcs" : "s3");
555
+ const prefix = options.prefix?.replace(/^\/+|\/+$/g, "") || undefined;
556
+ try {
557
+ await (await this.native()).mounts.add({
558
+ provider,
559
+ bucket,
560
+ path: mountPath,
561
+ ...(prefix ? { prefix } : {}),
562
+ ...(provider === "r2" && r2
563
+ ? { accountId: r2[1] }
564
+ : provider === "gcs"
565
+ ? {}
566
+ : { endpoint: `https://${host}` }),
567
+ ...(options.readOnly !== undefined ? { readOnly: options.readOnly } : {}),
568
+ });
569
+ }
570
+ catch (error) {
571
+ throw new S3FSMountError(`Failed to mount ${bucket} at ${mountPath}: ${error instanceof Error ? error.message : String(error)}`);
572
+ }
573
+ }
574
+ async unmountBucket(mountPath) {
575
+ try {
576
+ await (await this.native()).mounts.remove(mountPath);
577
+ }
578
+ catch (error) {
579
+ throw new BucketUnmountError(`Failed to unmount ${mountPath}: ${error instanceof Error ? error.message : String(error)}`);
580
+ }
581
+ }
582
+ createCodeContext(options) {
583
+ return this.#interpreter.createCodeContext(options);
584
+ }
585
+ async runCode(code, options = {}) {
586
+ if (hasEnv(options.envVars))
587
+ throw new CompatibilityError("Cloudflare", RUN_ENV);
588
+ return (await this.#interpreter.runCode(code, options)).toJSON();
589
+ }
590
+ runCodeStream(code, options = {}) {
591
+ if (hasEnv(options.envVars))
592
+ throw new CompatibilityError("Cloudflare", RUN_ENV);
593
+ return this.#interpreter.runCodeStream(code, options);
594
+ }
595
+ listCodeContexts() {
596
+ return this.#interpreter.listCodeContexts();
597
+ }
598
+ deleteCodeContext(contextId) {
599
+ return this.#interpreter.deleteCodeContext(contextId);
600
+ }
601
+ }
602
+ refuse("Cloudflare", "Sandbox", Sandbox, {
603
+ client: "Runtime's API is the client; use the Sandbox methods",
604
+ tunnels: "use exposePort for a public URL",
605
+ callTunnels: "use exposePort for a public URL",
606
+ setSandboxName: "the name is getSandbox's id",
607
+ setContainerTimeouts: "Runtime starts a sandbox in under a second, with nothing to tune",
608
+ setTransport: "Runtime's API is the only transport",
609
+ onStart: "a Runtime sandbox has no Durable Object lifecycle hooks",
610
+ onStop: "a Runtime sandbox has no Durable Object lifecycle hooks",
611
+ onActivityExpired: "set sleepAfter or keepAlive instead",
612
+ fetch: "fetch the URL exposePort returns",
613
+ containerFetch: "fetch the URL exposePort returns",
614
+ wsConnect: "connect to the URL exposePort returns",
615
+ getContainerPlacementId: "a Runtime sandbox's id is its place",
616
+ validatePortToken: "Runtime checks preview tokens itself",
617
+ createBackup: "snapshot the sandbox with Runtime's snapshots, or keep the folder on a volume",
618
+ restoreBackup: "create a sandbox from a Runtime snapshot",
619
+ });
620
+ refuse("Cloudflare", "Sandbox", Sandbox, { onError: "a Runtime sandbox has no Durable Object lifecycle hooks" }, { sync: true });
621
+ /** Cloudflare's Worker-side terminal proxy has no Runtime counterpart. */
622
+ export async function proxyTerminal(..._args) {
623
+ throw new CompatibilityError("Cloudflare", "proxyTerminal; open a terminal with the native Runtime sandbox's terminal()");
624
+ }
625
+ /** Runtime previews are served by Runtime, not through your Worker. */
626
+ export async function proxyToSandbox(..._args) {
627
+ throw new CompatibilityError("Cloudflare", "proxyToSandbox; Runtime serves the URLs exposePort returns itself");
265
628
  }
266
629
  export class ExecutionSession {
267
630
  sandbox;
@@ -283,7 +646,10 @@ export class ExecutionSession {
283
646
  "onComplete",
284
647
  "onError",
285
648
  "signal",
649
+ "origin",
286
650
  ]);
651
+ if (options.origin !== undefined && !["user", "internal"].includes(options.origin))
652
+ throw new TypeError("origin is user or internal");
287
653
  if (options.encoding && !["utf8", "utf-8"].includes(options.encoding))
288
654
  throw new CompatibilityError("Cloudflare", `process encoding ${options.encoding}`);
289
655
  const s = await this.sandbox.native();
@@ -413,6 +779,10 @@ export class ExecutionSession {
413
779
  })().catch((e) => options.onError?.(e instanceof Error ? e : new Error(String(e))));
414
780
  return process;
415
781
  }
782
+ /** A path as this session sees it: relative paths from its working directory. */
783
+ resolve(path) {
784
+ return this.path(path);
785
+ }
416
786
  async path(path) {
417
787
  if (path.startsWith("/"))
418
788
  return path;
@@ -566,13 +936,26 @@ export class Process {
566
936
  }
567
937
  async waitForExit(timeout) {
568
938
  const r = await this.native.wait({
569
- signal: timeout ? AbortSignal.timeout(timeout) : undefined,
939
+ signal: timeout ? timeoutSignal(timeout) : undefined,
570
940
  });
571
941
  return { exitCode: r.exitCode ?? -1 };
572
942
  }
573
943
  async waitForLog(pattern, timeout = 30000) {
944
+ const condition = `log pattern ${String(pattern)}`;
945
+ const context = { processId: this.id, command: this.command, condition };
946
+ const signal = timeoutSignal(timeout);
947
+ try {
948
+ return await this.#waitForLog(pattern, signal);
949
+ }
950
+ catch (error) {
951
+ if (!signal.aborted)
952
+ throw error;
953
+ throw new ProcessReadyTimeoutError(response("PROCESS_READY_TIMEOUT", `Process did not become ready within ${timeout}ms`, { ...context, timeout }, { httpStatus: 408, operation: "process.start" }));
954
+ }
955
+ }
956
+ async #waitForLog(pattern, signal) {
574
957
  let output = "";
575
- for await (const e of this.native.output({ signal: AbortSignal.timeout(timeout) })) {
958
+ for await (const e of this.native.output({ signal })) {
576
959
  if (e.type === "stdout" || e.type === "stderr") {
577
960
  output += e.data;
578
961
  const match = typeof pattern === "string" ? output.includes(pattern) : output.match(pattern);
@@ -586,7 +969,14 @@ export class Process {
586
969
  ...(typeof pattern === "string" ? {} : { match }),
587
970
  };
588
971
  }
972
+ else if (e.type === "exit")
973
+ throw new ProcessExitedBeforeReadyError(response("PROCESS_EXITED_BEFORE_READY", "Process exited before expected output", {
974
+ processId: this.id,
975
+ command: this.command,
976
+ condition: `log pattern ${String(pattern)}`,
977
+ exitCode: e.exitCode ?? -1,
978
+ }, { httpStatus: 409, operation: "process.start" }));
589
979
  }
590
- throw new Error("Process exited before expected output");
980
+ throw new Error("Process output ended before expected output");
591
981
  }
592
982
  }
@@ -0,0 +1,119 @@
1
+ import type { Sandbox as NativeSandbox } from "../sandbox.js";
2
+ type Language = "python" | "javascript" | "typescript";
3
+ export interface CreateContextOptions {
4
+ language?: Language;
5
+ cwd?: string;
6
+ envVars?: Record<string, string | undefined>;
7
+ timeout?: number;
8
+ }
9
+ export interface CodeContext {
10
+ readonly id: string;
11
+ readonly language: string;
12
+ readonly cwd: string;
13
+ readonly createdAt: Date;
14
+ readonly lastUsed: Date;
15
+ }
16
+ export interface OutputMessage {
17
+ text: string;
18
+ timestamp: number;
19
+ }
20
+ export interface ExecutionError {
21
+ name: string;
22
+ message: string;
23
+ traceback: string[];
24
+ lineNumber?: number;
25
+ }
26
+ export interface RunCodeOptions {
27
+ context?: CodeContext;
28
+ language?: Language;
29
+ envVars?: Record<string, string | undefined>;
30
+ timeout?: number;
31
+ signal?: AbortSignal;
32
+ onStdout?: (output: OutputMessage) => void | Promise<void>;
33
+ onStderr?: (output: OutputMessage) => void | Promise<void>;
34
+ onResult?: (result: Result) => void | Promise<void>;
35
+ onError?: (error: ExecutionError) => void | Promise<void>;
36
+ }
37
+ declare const FORMATS: {
38
+ readonly text: "text/plain";
39
+ readonly html: "text/html";
40
+ readonly png: "image/png";
41
+ readonly jpeg: "image/jpeg";
42
+ readonly svg: "image/svg+xml";
43
+ readonly latex: "text/latex";
44
+ readonly markdown: "text/markdown";
45
+ readonly javascript: "application/javascript";
46
+ readonly json: "application/json";
47
+ };
48
+ type Fields = {
49
+ -readonly [K in keyof typeof FORMATS]?: K extends "json" ? unknown : string;
50
+ };
51
+ /** One rich result, as Cloudflare's: each format by name, the MIME bundle in
52
+ * `data`. `chart` is never set on Runtime. */
53
+ export declare class Result implements Fields {
54
+ text?: string;
55
+ html?: string;
56
+ png?: string;
57
+ jpeg?: string;
58
+ svg?: string;
59
+ latex?: string;
60
+ markdown?: string;
61
+ javascript?: string;
62
+ json?: unknown;
63
+ chart?: undefined;
64
+ data?: Record<string, unknown>;
65
+ constructor(data: Record<string, unknown>);
66
+ formats(): string[];
67
+ }
68
+ export declare class Execution {
69
+ readonly code: string;
70
+ readonly context: CodeContext;
71
+ results: Result[];
72
+ logs: {
73
+ stdout: string[];
74
+ stderr: string[];
75
+ };
76
+ error?: ExecutionError;
77
+ executionCount?: number;
78
+ constructor(code: string, context: CodeContext);
79
+ toJSON(): {
80
+ code: string;
81
+ logs: {
82
+ stdout: string[];
83
+ stderr: string[];
84
+ };
85
+ error: ExecutionError | undefined;
86
+ executionCount: number | undefined;
87
+ results: {
88
+ text: string | undefined;
89
+ html: string | undefined;
90
+ png: string | undefined;
91
+ jpeg: string | undefined;
92
+ svg: string | undefined;
93
+ latex: string | undefined;
94
+ markdown: string | undefined;
95
+ javascript: string | undefined;
96
+ json: unknown;
97
+ chart: undefined;
98
+ data: Record<string, unknown> | undefined;
99
+ }[];
100
+ };
101
+ }
102
+ export declare const RUN_ENV = "envVars on a run; set them when making the context with createCodeContext({ envVars })";
103
+ export declare const hasEnv: (env: Record<string, string | undefined> | undefined) => boolean;
104
+ /** Cloudflare's code interpreter on Runtime's: Python, JavaScript and
105
+ * TypeScript contexts whose variables persist between runs. */
106
+ export declare class CodeInterpreter {
107
+ #private;
108
+ constructor(sandbox: {
109
+ native(): Promise<NativeSandbox>;
110
+ } | (() => Promise<NativeSandbox>));
111
+ createCodeContext(options?: CreateContextOptions): Promise<CodeContext>;
112
+ runCode(code: string, options?: RunCodeOptions): Promise<Execution>;
113
+ /** The run as Cloudflare's container streams it: `data:` lines of stdout,
114
+ * stderr, result, error and execution_complete events. */
115
+ runCodeStream(code: string, options?: RunCodeOptions): Promise<ReadableStream>;
116
+ listCodeContexts(): Promise<CodeContext[]>;
117
+ deleteCodeContext(contextId: string): Promise<void>;
118
+ }
119
+ export {};