withruntime 0.9.0 → 0.11.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 (124) hide show
  1. package/BLAXEL.md +4 -4
  2. package/DAYTONA.md +52 -40
  3. package/E2B.md +65 -62
  4. package/README.md +15 -7
  5. package/README.src.md +15 -7
  6. package/VERCEL.md +34 -22
  7. package/compatibility-lock.json +16 -16
  8. package/dist/blaxel/errors.js +1 -1
  9. package/dist/blaxel/lookup.d.ts +3 -2
  10. package/dist/blaxel/lookup.js +3 -2
  11. package/dist/blaxel/pagination.d.ts +2 -2
  12. package/dist/blaxel/pagination.js +45 -17
  13. package/dist/blaxel/preview.d.ts +2 -1
  14. package/dist/blaxel/preview.js +15 -13
  15. package/dist/blaxel/process.js +56 -10
  16. package/dist/blaxel/sandbox.d.ts +2 -0
  17. package/dist/blaxel/sandbox.js +2 -0
  18. package/dist/blaxel/snapshot.js +3 -5
  19. package/dist/cli.d.ts +2 -0
  20. package/dist/cli.js +262 -56
  21. package/dist/client.d.ts +41 -3
  22. package/dist/client.js +67 -11
  23. package/dist/cloudflare/identity.d.ts +7 -0
  24. package/dist/cloudflare/identity.js +13 -0
  25. package/dist/cloudflare/index.d.ts +9 -0
  26. package/dist/cloudflare/index.js +47 -9
  27. package/dist/cloudflare/worker.d.ts +4 -0
  28. package/dist/cloudflare/worker.js +166 -16
  29. package/dist/codesandbox/guest-service.js +57 -20
  30. package/dist/codesandbox/index.js +42 -8
  31. package/dist/compat/core.d.ts +9 -8
  32. package/dist/compat/core.js +60 -21
  33. package/dist/compat/files.js +22 -4
  34. package/dist/compat/lease.d.ts +36 -0
  35. package/dist/compat/lease.js +87 -0
  36. package/dist/credentials.d.ts +6 -1
  37. package/dist/credentials.js +107 -38
  38. package/dist/daytona/computer-use.d.ts +162 -0
  39. package/dist/daytona/computer-use.js +324 -0
  40. package/dist/daytona/context.d.ts +10 -0
  41. package/dist/daytona/context.js +24 -0
  42. package/dist/daytona/daytona.d.ts +20 -0
  43. package/dist/daytona/daytona.js +134 -20
  44. package/dist/daytona/filesystem.d.ts +17 -4
  45. package/dist/daytona/filesystem.js +129 -38
  46. package/dist/daytona/index.d.ts +3 -27
  47. package/dist/daytona/index.js +2 -7
  48. package/dist/daytona/process.d.ts +63 -6
  49. package/dist/daytona/process.js +224 -23
  50. package/dist/daytona/sandbox.d.ts +20 -6
  51. package/dist/daytona/sandbox.js +55 -32
  52. package/dist/e2b/client.d.ts +5 -0
  53. package/dist/e2b/client.js +6 -2
  54. package/dist/e2b/commands.d.ts +43 -12
  55. package/dist/e2b/commands.js +217 -64
  56. package/dist/e2b/errors.d.ts +7 -0
  57. package/dist/e2b/errors.js +20 -0
  58. package/dist/e2b/filesystem.d.ts +2 -1
  59. package/dist/e2b/filesystem.js +43 -6
  60. package/dist/e2b/index.d.ts +3 -2
  61. package/dist/e2b/index.js +2 -1
  62. package/dist/e2b/pty.d.ts +24 -0
  63. package/dist/e2b/pty.js +83 -0
  64. package/dist/e2b/sandbox.d.ts +17 -6
  65. package/dist/e2b/sandbox.js +239 -43
  66. package/dist/e2b/users.d.ts +30 -0
  67. package/dist/e2b/users.js +185 -0
  68. package/dist/file-source.d.ts +16 -0
  69. package/dist/file-source.js +162 -0
  70. package/dist/freestyle/index.d.ts +9 -1
  71. package/dist/freestyle/index.js +38 -5
  72. package/dist/index.d.ts +3 -1
  73. package/dist/login.js +16 -10
  74. package/dist/mcp.d.ts +2 -1
  75. package/dist/mcp.js +6 -1
  76. package/dist/modal/index.js +2 -1
  77. package/dist/network-cli.js +139 -43
  78. package/dist/openai-agents/index.js +9 -45
  79. package/dist/page.d.ts +1 -1
  80. package/dist/page.js +2 -2
  81. package/dist/products/browser.d.ts +60 -0
  82. package/dist/products/browser.js +37 -0
  83. package/dist/products/desktop.d.ts +2 -2
  84. package/dist/products/desktop.js +2 -2
  85. package/dist/products/images.d.ts +3 -1
  86. package/dist/products/images.js +14 -2
  87. package/dist/products/index.d.ts +14 -0
  88. package/dist/products/index.js +12 -0
  89. package/dist/products/network-products.d.ts +7 -0
  90. package/dist/products/network-products.js +3 -0
  91. package/dist/products/observability.d.ts +44 -0
  92. package/dist/products/observability.js +6 -0
  93. package/dist/products/previews.d.ts +8 -2
  94. package/dist/products/previews.js +11 -3
  95. package/dist/products/private-network.d.ts +22 -0
  96. package/dist/products/private-network.js +10 -0
  97. package/dist/products/volumes.d.ts +42 -0
  98. package/dist/products/volumes.js +36 -0
  99. package/dist/products/watch.js +61 -8
  100. package/dist/proxy.js +3 -1
  101. package/dist/runloop/index.d.ts +13 -27
  102. package/dist/runloop/index.js +149 -52
  103. package/dist/sandbox.d.ts +59 -8
  104. package/dist/sandbox.js +479 -68
  105. package/dist/snapshots.d.ts +9 -4
  106. package/dist/sprites/index.js +69 -33
  107. package/dist/tar.d.ts +3 -1
  108. package/dist/tar.js +140 -14
  109. package/dist/transport.d.ts +4 -7
  110. package/dist/transport.js +205 -122
  111. package/dist/types.d.ts +5 -2
  112. package/dist/vercel/command.d.ts +1 -1
  113. package/dist/vercel/command.js +3 -2
  114. package/dist/vercel/errors.d.ts +1 -1
  115. package/dist/vercel/errors.js +7 -1
  116. package/dist/vercel/index.d.ts +1 -7
  117. package/dist/vercel/index.js +1 -3
  118. package/dist/vercel/sandbox-user.d.ts +70 -0
  119. package/dist/vercel/sandbox-user.js +206 -0
  120. package/dist/vercel/sandbox.d.ts +55 -20
  121. package/dist/vercel/sandbox.js +353 -99
  122. package/dist/vercel/snapshot.d.ts +3 -2
  123. package/dist/vercel/snapshot.js +9 -5
  124. package/package.json +2 -1
@@ -0,0 +1,30 @@
1
+ import type { FileEntry } from "../types.js";
2
+ import type { RequestOptions } from "../transport.js";
3
+ import type { SandboxContext, Username } from "./commands.js";
4
+ /** The Linux user a call runs as, or undefined for the sandbox's own. Refuses
5
+ * a user the sandbox does not have, once per user and sandbox. */
6
+ export declare function runAs(ctx: SandboxContext, user: Username | undefined, options: RequestOptions): Promise<string | undefined>;
7
+ /** `bash -c script` as `user`, keeping the environment Runtime gives the
8
+ * command. Without a `cwd` it starts in the user's home, as E2B's does. */
9
+ export declare function commandAs(user: string, script: string, cwdGiven: boolean): string[];
10
+ /** An interactive login shell as `user`, for a PTY. */
11
+ export declare function shellAs(user: string, cwdGiven: boolean): string[];
12
+ /** E2B's file calls as another Linux user: each one a short script run with
13
+ * `sudo -u`, since Runtime's file API acts as the sandbox's own user. Output
14
+ * streams, so a read of any size comes back whole. */
15
+ export declare class FilesAs {
16
+ #private;
17
+ private readonly ctx;
18
+ private readonly user;
19
+ private readonly options;
20
+ constructor(ctx: SandboxContext, user: string, options: RequestOptions);
21
+ read(path: string): Promise<Uint8Array>;
22
+ write(path: string, bytes: Uint8Array): Promise<void>;
23
+ list(path: string, depth: number): Promise<FileEntry[]>;
24
+ stat(path: string): Promise<FileEntry>;
25
+ exists(path: string): Promise<boolean>;
26
+ /** False when it was there already. */
27
+ makeDir(path: string): Promise<boolean>;
28
+ rename(from: string, to: string): Promise<void>;
29
+ remove(path: string): Promise<void>;
30
+ }
@@ -0,0 +1,185 @@
1
+ import { FileNotFoundError, guard, InvalidArgumentError, SandboxError } from "./errors.js";
2
+ /* E2B's `user` option, on Runtime. A sandbox's own user is `runtime`, with
3
+ passwordless sudo; E2B's default user is called "user". So "user" (or no
4
+ user) runs as the sandbox's own user, and any other name runs through
5
+ `sudo -u <name>`. The user must already exist: the adapter never makes one,
6
+ and a name the sandbox does not know is refused before anything runs. */
7
+ const NAME = /^[a-z_][a-z0-9_.-]{0,31}$/i;
8
+ /** Exit codes the scripts below use to say what went wrong. */
9
+ const MISSING = 44;
10
+ const NOT_DIRECTORY = 45;
11
+ const IS_DIRECTORY = 46;
12
+ const EXISTS = 47;
13
+ /** Base64 text per exec: under the 1 MiB an exec's input may carry. */
14
+ const WRITE_CHUNK = 786_432;
15
+ /** Only the caller's signal: an exec's own `timeoutMs` is the command's. */
16
+ const signalOf = (options) => (options.signal ? { signal: options.signal } : {});
17
+ const checked = new WeakMap();
18
+ /** The Linux user a call runs as, or undefined for the sandbox's own. Refuses
19
+ * a user the sandbox does not have, once per user and sandbox. */
20
+ export async function runAs(ctx, user, options) {
21
+ if (user === undefined || user === "user")
22
+ return undefined;
23
+ if (typeof user !== "string" || !NAME.test(user))
24
+ throw new InvalidArgumentError(`"${String(user)}" is not a Linux user name.`);
25
+ if (user === "root")
26
+ return user;
27
+ let users = checked.get(ctx);
28
+ if (!users)
29
+ checked.set(ctx, (users = new Map()));
30
+ let check = users.get(user);
31
+ if (!check) {
32
+ check = (async () => {
33
+ const found = await guard("sandbox", () => ctx.runtime.exec(["id", "-u", "--", user], signalOf(options)));
34
+ if (found.exitCode !== 0)
35
+ throw new InvalidArgumentError(`This sandbox has no user "${user}", and Runtime never creates one for you. ` +
36
+ `Make it first, as root: sandbox.commands.run("useradd -m ${user}", { user: "root" }). ` +
37
+ `"user" is the sandbox's own user and "root" is root.`);
38
+ })();
39
+ users.set(user, check);
40
+ check.catch(() => users.delete(user));
41
+ }
42
+ await check;
43
+ return user;
44
+ }
45
+ /** `bash -c script` as `user`, keeping the environment Runtime gives the
46
+ * command. Without a `cwd` it starts in the user's home, as E2B's does. */
47
+ export function commandAs(user, script, cwdGiven) {
48
+ return [
49
+ "sudo",
50
+ "-n",
51
+ "-E",
52
+ "-H",
53
+ "-u",
54
+ user,
55
+ "--",
56
+ "/bin/bash",
57
+ "-c",
58
+ cwdGiven ? script : `cd ~ 2>/dev/null\n${script}`,
59
+ ];
60
+ }
61
+ /** An interactive login shell as `user`, for a PTY. */
62
+ export function shellAs(user, cwdGiven) {
63
+ return commandAs(user, "exec /bin/bash -i -l", cwdGiven);
64
+ }
65
+ /** One `sh -c` script as `user`, with its arguments passed apart, never
66
+ * pasted into the script. */
67
+ function script(user, text, ...args) {
68
+ return ["sudo", "-n", "-u", user, "--", "/bin/sh", "-c", text, "sh", ...args];
69
+ }
70
+ /** Each entry `find -printf` prints: eight fields, each ended by NUL. */
71
+ const FIELDS = "%y\\0%s\\0%m\\0%u\\0%g\\0%T@\\0%p\\0%l\\0";
72
+ const TYPES = { f: "file", d: "directory", l: "symlink" };
73
+ function entries(output) {
74
+ const fields = output.split("\0");
75
+ const out = [];
76
+ for (let i = 0; i + 8 <= fields.length; i += 8) {
77
+ const [kind, size, mode, owner, group, mtime, path, link] = fields.slice(i, i + 8);
78
+ out.push({
79
+ name: path.split("/").filter(Boolean).pop() ?? path,
80
+ path,
81
+ type: TYPES[kind] ?? "other",
82
+ size: Number(size) || 0,
83
+ mode: `0${mode}`,
84
+ modifiedAt: new Date(Math.round(Number(mtime) * 1000)).toISOString(),
85
+ owner,
86
+ group,
87
+ ...(kind === "l" && link ? { symlinkTarget: link } : {}),
88
+ });
89
+ }
90
+ return out;
91
+ }
92
+ /** E2B's file calls as another Linux user: each one a short script run with
93
+ * `sudo -u`, since Runtime's file API acts as the sandbox's own user. Output
94
+ * streams, so a read of any size comes back whole. */
95
+ export class FilesAs {
96
+ ctx;
97
+ user;
98
+ options;
99
+ constructor(ctx, user, options) {
100
+ this.ctx = ctx;
101
+ this.user = user;
102
+ this.options = options;
103
+ }
104
+ async #run(argv, stdin) {
105
+ const result = await guard("sandbox", () => this.ctx.runtime.exec(argv, {
106
+ ...(stdin === undefined ? {} : { stdin }),
107
+ // A callback makes the exec stream, so its output is never cut.
108
+ onStdout: () => undefined,
109
+ ...signalOf(this.options),
110
+ }));
111
+ if (result.stdoutTruncated)
112
+ throw new SandboxError("Part of the answer was lost on the way; try again.");
113
+ return result;
114
+ }
115
+ #fail(path, result) {
116
+ if (result.exitCode === MISSING)
117
+ throw new FileNotFoundError(`${path} does not exist.`);
118
+ if (result.exitCode === NOT_DIRECTORY)
119
+ throw new InvalidArgumentError(`${path} is not a directory.`);
120
+ if (result.exitCode === IS_DIRECTORY)
121
+ throw new InvalidArgumentError(`${path} is a directory.`);
122
+ throw new SandboxError(`As the user "${this.user}": ${result.stderr.trim() || `exit status ${result.exitCode}`}`);
123
+ }
124
+ async read(path) {
125
+ const result = await this.#run(script(this.user, `[ -e "$1" ] || exit ${MISSING}; [ -d "$1" ] && exit ${IS_DIRECTORY}; exec base64 -w 0 -- "$1"`, path));
126
+ if (result.exitCode !== 0)
127
+ this.#fail(path, result);
128
+ return Uint8Array.from(atob(result.stdout.trim()), (c) => c.charCodeAt(0));
129
+ }
130
+ async write(path, bytes) {
131
+ let text = "";
132
+ for (let i = 0; i < bytes.length; i += 32_768)
133
+ text += String.fromCharCode(...bytes.subarray(i, i + 32_768));
134
+ const encoded = btoa(text);
135
+ let offset = 0;
136
+ do {
137
+ const chunk = encoded.slice(offset, offset + WRITE_CHUNK);
138
+ const result = await this.#run(script(this.user, offset === 0
139
+ ? `mkdir -p -- "$(dirname -- "$1")" && base64 -d > "$1"`
140
+ : `base64 -d >> "$1"`, path), chunk);
141
+ if (result.exitCode !== 0)
142
+ this.#fail(path, result);
143
+ offset += WRITE_CHUNK;
144
+ } while (offset < encoded.length);
145
+ }
146
+ async list(path, depth) {
147
+ const result = await this.#run(script(this.user, `[ -e "$1" ] || exit ${MISSING}; [ -d "$1" ] || exit ${NOT_DIRECTORY}; ` +
148
+ `exec find "$1" -mindepth 1 -maxdepth "$2" -printf '${FIELDS}'`, path, String(depth)));
149
+ if (result.exitCode !== 0)
150
+ this.#fail(path, result);
151
+ return entries(result.stdout);
152
+ }
153
+ async stat(path) {
154
+ const result = await this.#run(script(this.user, `[ -e "$1" ] || [ -L "$1" ] || exit ${MISSING}; exec find "$1" -maxdepth 0 -printf '${FIELDS}'`, path));
155
+ if (result.exitCode !== 0)
156
+ this.#fail(path, result);
157
+ const [entry] = entries(result.stdout);
158
+ if (!entry)
159
+ throw new FileNotFoundError(`${path} does not exist.`);
160
+ return entry;
161
+ }
162
+ async exists(path) {
163
+ const result = await this.#run(script(this.user, `[ -e "$1" ] || [ -L "$1" ]`, path));
164
+ return result.exitCode === 0;
165
+ }
166
+ /** False when it was there already. */
167
+ async makeDir(path) {
168
+ const result = await this.#run(script(this.user, `[ -d "$1" ] && exit ${EXISTS}; mkdir -p -- "$1"`, path));
169
+ if (result.exitCode === EXISTS)
170
+ return false;
171
+ if (result.exitCode !== 0)
172
+ this.#fail(path, result);
173
+ return true;
174
+ }
175
+ async rename(from, to) {
176
+ const result = await this.#run(script(this.user, `[ -e "$1" ] || [ -L "$1" ] || exit ${MISSING}; mv -fT -- "$1" "$2"`, from, to));
177
+ if (result.exitCode !== 0)
178
+ this.#fail(from, result);
179
+ }
180
+ async remove(path) {
181
+ const result = await this.#run(script(this.user, `rm -rf -- "$1"`, path));
182
+ if (result.exitCode !== 0)
183
+ this.#fail(path, result);
184
+ }
185
+ }
@@ -0,0 +1,16 @@
1
+ export type StreamUploadSource = string | Blob | AsyncIterable<Uint8Array> | ReadableStream<Uint8Array>;
2
+ export type UploadScope = {
3
+ signal?: AbortSignal;
4
+ check(): void;
5
+ wait<T>(pending: Promise<T>): Promise<T>;
6
+ };
7
+ export type PreparedFileSource = {
8
+ size: number;
9
+ sha256: string;
10
+ chunkAt(offset: number, length: number): Promise<Uint8Array>;
11
+ close(): Promise<void>;
12
+ };
13
+ /** ARCHITECTURE.md section 10, Folders over HTTP: unknown-length inputs use
14
+ * bounded private disk staging; known files and Blobs remain repeatable inputs.
15
+ * Size and digest are established before the remote upload can begin. */
16
+ export declare function prepareFileSource(input: StreamUploadSource, scope: UploadScope): Promise<PreparedFileSource>;
@@ -0,0 +1,162 @@
1
+ import { RuntimeError } from "./errors.js";
2
+ const CHUNK = 1_048_576;
3
+ function failure(message, code, cause) {
4
+ return new RuntimeError({ message, code, status: 0, ...(cause === undefined ? {} : { cause }) });
5
+ }
6
+ async function exact(handle, offset, length) {
7
+ const data = new Uint8Array(length);
8
+ let held = 0;
9
+ while (held < length) {
10
+ const read = await handle.read(data, held, length - held, offset + held);
11
+ if (read.bytesRead === 0)
12
+ throw failure("The local upload source changed or ended before its declared size.", "source_changed");
13
+ held += read.bytesRead;
14
+ }
15
+ return data;
16
+ }
17
+ /** ARCHITECTURE.md section 10, Folders over HTTP: unknown-length inputs use
18
+ * bounded private disk staging; known files and Blobs remain repeatable inputs.
19
+ * Size and digest are established before the remote upload can begin. */
20
+ export async function prepareFileSource(input, scope) {
21
+ const fs = await import("node:fs/promises");
22
+ const { createHash } = await import("node:crypto");
23
+ const { Readable } = await import("node:stream");
24
+ scope.check();
25
+ const hash = createHash("sha256");
26
+ let size = 0;
27
+ let handle;
28
+ let staging;
29
+ let iterator;
30
+ // What is used of a reader, so the code compiles against Node's stream types
31
+ // as well as the DOM's (packages/cloud compiles this file without DOM).
32
+ let reader;
33
+ const cleanup = async () => {
34
+ try {
35
+ await handle?.close();
36
+ }
37
+ finally {
38
+ if (staging)
39
+ await fs.rm(staging, { recursive: true, force: true });
40
+ }
41
+ };
42
+ try {
43
+ if (typeof input === "string") {
44
+ handle = await fs.open(input, "r");
45
+ const info = await handle.stat();
46
+ if (!info.isFile() || !Number.isSafeInteger(info.size))
47
+ throw failure("A streamed file upload requires a regular local file.", "invalid_upload_source");
48
+ size = info.size;
49
+ for (let offset = 0; offset < size; offset += CHUNK) {
50
+ scope.check();
51
+ hash.update(await scope.wait(exact(handle, offset, Math.min(CHUNK, size - offset))));
52
+ }
53
+ }
54
+ else if (input instanceof Blob) {
55
+ // Bun's types leave out Blob.slice when the DOM library is off, as it is
56
+ // where packages/cloud compiles this file; every runtime has it.
57
+ const blob = input;
58
+ size = blob.size;
59
+ for (let offset = 0; offset < size; offset += CHUNK) {
60
+ scope.check();
61
+ hash.update(new Uint8Array(await scope.wait(blob.slice(offset, offset + CHUNK).arrayBuffer())));
62
+ }
63
+ return {
64
+ size,
65
+ sha256: hash.digest("hex"),
66
+ chunkAt: async (offset, length) => new Uint8Array(await blob.slice(offset, offset + length).arrayBuffer()),
67
+ close: async () => undefined,
68
+ };
69
+ }
70
+ else {
71
+ if (!input ||
72
+ (typeof input.getReader !== "function" &&
73
+ typeof input[Symbol.asyncIterator] !== "function"))
74
+ throw failure("A streamed upload requires a byte stream or local file.", "invalid_upload_source");
75
+ const { tmpdir } = await import("node:os");
76
+ const path = await import("node:path");
77
+ staging = await fs.mkdtemp(path.join(tmpdir(), "runtime-upload-"));
78
+ await fs.chmod(staging, 0o700);
79
+ handle = await fs.open(path.join(staging, "source"), "wx+", 0o600);
80
+ const space = await fs.statfs(staging, { bigint: true });
81
+ const available = space.bavail * space.bsize;
82
+ if ("getReader" in input)
83
+ reader = input.getReader();
84
+ else
85
+ iterator = input[Symbol.asyncIterator]();
86
+ for (;;) {
87
+ scope.check();
88
+ const next = await scope.wait(reader ? reader.read() : iterator.next());
89
+ if (next.done)
90
+ break;
91
+ if (!(next.value instanceof Uint8Array))
92
+ throw failure("A streamed upload must produce byte chunks.", "invalid_upload_source");
93
+ for (let offset = 0; offset < next.value.length; offset += CHUNK) {
94
+ scope.check();
95
+ const part = next.value.subarray(offset, offset + CHUNK);
96
+ if (!Number.isSafeInteger(size + part.length))
97
+ throw failure("The streamed upload exceeds the native file size range.", "upload_too_large");
98
+ if (BigInt(size + part.length) > available)
99
+ throw failure("There is not enough local temporary disk space to stage the streamed upload.", "local_disk_full");
100
+ let written = 0;
101
+ while (written < part.length) {
102
+ scope.check();
103
+ const result = await handle.write(part, written, part.length - written, size + written);
104
+ if (result.bytesWritten === 0)
105
+ throw failure("The local temporary file accepted no upload bytes.", "local_disk_full");
106
+ written += result.bytesWritten;
107
+ }
108
+ size += part.length;
109
+ hash.update(part);
110
+ }
111
+ }
112
+ }
113
+ scope.check();
114
+ const owned = handle;
115
+ if (!owned)
116
+ throw failure("The local upload file did not open.", "invalid_upload_source");
117
+ return {
118
+ size,
119
+ sha256: hash.digest("hex"),
120
+ chunkAt: (offset, length) => exact(owned, offset, length),
121
+ close: cleanup,
122
+ };
123
+ }
124
+ catch (error) {
125
+ // Calling return/cancel can itself throw synchronously. Cleanup still owns
126
+ // the file and directory, and must preserve the original source failure.
127
+ let timer;
128
+ try {
129
+ // Node's pending next() must settle before iterator.return() can finish.
130
+ // Web readers own cancellation directly; arbitrary iterators may not.
131
+ if (input instanceof Readable)
132
+ input.destroy();
133
+ const closing = reader ? reader.cancel(scope.signal?.reason) : iterator?.return?.();
134
+ if (closing)
135
+ await Promise.race([
136
+ closing,
137
+ new Promise((resolve) => {
138
+ timer = setTimeout(resolve, 1000);
139
+ }),
140
+ ]);
141
+ }
142
+ catch {
143
+ /* Preserve the caller's original failure. */
144
+ }
145
+ finally {
146
+ if (timer)
147
+ clearTimeout(timer);
148
+ await cleanup().catch(() => undefined);
149
+ }
150
+ if (error?.code === "ENOSPC")
151
+ throw failure("There is not enough local temporary disk space to stage the streamed upload.", "local_disk_full", error);
152
+ throw error;
153
+ }
154
+ finally {
155
+ try {
156
+ reader?.releaseLock();
157
+ }
158
+ catch {
159
+ /* Cancellation may leave a pending caller-owned read. */
160
+ }
161
+ }
162
+ }
@@ -38,22 +38,29 @@ export declare class VmsNamespace {
38
38
  id: string;
39
39
  sourceVmId: string;
40
40
  slug: string | null;
41
+ displayName: string | null;
41
42
  public: boolean;
42
43
  createdAt: string;
43
44
  updatedAt: string;
44
45
  ttlSeconds: number;
45
46
  }>;
46
47
  delete: (id: string) => Promise<void>;
47
- list: () => Promise<{
48
+ list: (options?: {
49
+ sourceVmId?: string;
50
+ limit?: number;
51
+ offset?: number;
52
+ }) => Promise<{
48
53
  snapshots: {
49
54
  id: string;
50
55
  sourceVmId: string;
51
56
  slug: string | null;
57
+ displayName: string | null;
52
58
  public: boolean;
53
59
  createdAt: string;
54
60
  updatedAt: string;
55
61
  ttlSeconds: number;
56
62
  }[];
63
+ totalCount: number;
57
64
  }>;
58
65
  };
59
66
  constructor(runtime: Runtime);
@@ -292,6 +299,7 @@ export declare class Vm {
292
299
  id: string;
293
300
  sourceVmId: string;
294
301
  slug: string | null;
302
+ displayName: string | null;
295
303
  public: boolean;
296
304
  createdAt: string;
297
305
  updatedAt: string;
@@ -1,6 +1,11 @@
1
1
  import { all, client, CompatibilityError, create, destroy, execOptions, lookup, only, ready, } from "../compat/core.js";
2
2
  import { GuestFiles } from "../compat/files.js";
3
3
  export { CompatibilityError };
4
+ function validateMetadata(metadata) {
5
+ for (const key of ["compat.provider", "fs.displayName"])
6
+ if (metadata !== undefined && Object.prototype.hasOwnProperty.call(metadata, key))
7
+ throw new CompatibilityError("Freestyle", `metadata key ${key} reserved by this adapter`);
8
+ }
4
9
  const data = (s) => ({
5
10
  id: s.id,
6
11
  state: s.state,
@@ -18,6 +23,7 @@ const snapshotData = (s) => ({
18
23
  id: s.id,
19
24
  sourceVmId: s.sourceSandboxId,
20
25
  slug: s.name,
26
+ displayName: s.labels?.["fs.displayName"] ?? null,
21
27
  public: false,
22
28
  createdAt: s.createdAt,
23
29
  updatedAt: s.createdAt,
@@ -37,9 +43,32 @@ export class VmsNamespace {
37
43
  this.snapshots = {
38
44
  get: async (id) => snapshotData(await this.resolveSnapshot(id)),
39
45
  delete: async (id) => runtime.snapshots.delete((await this.resolveSnapshot(id)).id),
40
- list: async () => ({
41
- snapshots: (await (await runtime.snapshots.list()).toArray()).map(snapshotData),
42
- }),
46
+ list: async (options = {}) => {
47
+ only("Freestyle snapshot list", options, ["sourceVmId", "limit", "offset"]);
48
+ if (options.sourceVmId !== undefined && typeof options.sourceVmId !== "string")
49
+ throw new TypeError("sourceVmId must be a string");
50
+ if (options.limit !== undefined &&
51
+ (!Number.isSafeInteger(options.limit) || options.limit <= 0))
52
+ throw new TypeError("limit must be a positive safe integer");
53
+ if (options.offset !== undefined &&
54
+ (!Number.isSafeInteger(options.offset) || options.offset < 0))
55
+ throw new TypeError("offset must be a nonnegative safe integer");
56
+ const offset = options.offset ?? 0;
57
+ const snapshots = [];
58
+ let totalCount = 0;
59
+ // Count the filtered catalog across every native page without retaining
60
+ // snapshots outside the requested page or Page.toArray's default cap.
61
+ const page = await runtime.snapshots.list(options.sourceVmId === undefined ? {} : { sandboxId: options.sourceVmId });
62
+ for await (const snapshot of page) {
63
+ if (options.sourceVmId !== undefined && snapshot.sourceSandboxId !== options.sourceVmId)
64
+ continue;
65
+ if (totalCount >= offset &&
66
+ (options.limit === undefined || snapshots.length < options.limit))
67
+ snapshots.push(snapshotData(snapshot));
68
+ totalCount++;
69
+ }
70
+ return { snapshots, totalCount };
71
+ },
43
72
  };
44
73
  }
45
74
  async resolveSnapshot(id) {
@@ -59,6 +88,7 @@ export class VmsNamespace {
59
88
  "metadata",
60
89
  "firewall",
61
90
  ]);
91
+ validateMetadata(options.metadata);
62
92
  if (!options.firewall || !Array.isArray(options.firewall.rules))
63
93
  throw new TypeError("firewall.rules is required");
64
94
  only("Freestyle firewall", options.firewall, ["rules"]);
@@ -266,6 +296,7 @@ export class Vm {
266
296
  }
267
297
  async update(options) {
268
298
  only("Freestyle update", options, ["slug", "displayName", "idleTimeoutSeconds", "metadata"]);
299
+ validateMetadata(options.metadata);
269
300
  const s = await lookup(this.runtime, this.id);
270
301
  await s.update({
271
302
  name: options.slug,
@@ -298,8 +329,10 @@ export class Vm {
298
329
  async snapshot(options = {}) {
299
330
  only("Freestyle snapshot", options, ["slug", "displayName", "ttlSeconds"]);
300
331
  if (options.ttlSeconds !== undefined &&
301
- (options.ttlSeconds <= 0 || options.ttlSeconds % 86400 !== 0))
302
- throw new CompatibilityError("Freestyle", "snapshot retention other than whole positive days");
332
+ (options.ttlSeconds <= 0 ||
333
+ options.ttlSeconds > 365 * 86400 ||
334
+ options.ttlSeconds % 86400 !== 0))
335
+ throw new CompatibilityError("Freestyle", "snapshot retention outside 1 to 365 whole days");
303
336
  const s = await (await lookup(this.runtime, this.id)).snapshot({
304
337
  name: options.slug,
305
338
  labels: options.displayName ? { "fs.displayName": options.displayName } : undefined,
package/dist/index.d.ts CHANGED
@@ -15,15 +15,17 @@ export { Tunnel, TunnelStream, type PortForward } from "./tunnel.js";
15
15
  export { Page } from "./page.js";
16
16
  export type { KeyLimits } from "./products/limits.js";
17
17
  export type { AuditEvent, AuditPage } from "./products/audit.js";
18
+ export type { UsageExportQuery, UsageExportRow, UsageExportPage, } from "./products/observability.js";
18
19
  export type { JobSecret, Secret, SecretRule, SecretUse, SetSecret } from "./products/secrets.js";
19
20
  export { SecretPartlyStoredError } from "./products/secrets.js";
20
21
  export type { McpCatalogEntry, McpGateway, McpServerRequest } from "./products/mcp.js";
21
22
  export type { FileEvent, FileEventType, WatchHandle, WatchNotice, WatchOptions, } from "./products/watch.js";
22
23
  export type { Mount, MountBucket } from "./products/mounts.js";
23
24
  export type { TailscaleJoin, TailscaleJoined, TailscaleNode, TailscaleStatus, } from "./products/tailscale.js";
24
- export type { Volume, VolumeBackup, CreateVolume } from "./products/volumes.js";
25
+ export type { Volume, VolumeBackup, CreateVolume, VolumeAttachment } from "./products/volumes.js";
25
26
  export type { CreateJob, Job, JobCompute, JobDefinition, JobLogs, JobRun, JobRunState, JobSchedule, } from "./products/jobs.js";
26
27
  export type { CheckedDomain, DnsRecord, Domain, EgressAddress, TcpPort, Tunnel as PrivateTunnel, TunnelPeer, TunnelPeerCreated, SetUpstreamProxy, UpstreamProxy, } from "./products/network-products.js";
28
+ export type { PrivateNetwork } from "./products/private-network.js";
27
29
  export { verifyWebhook, WebhookVerificationError, type MetricPoint, type MetricRange, type OtelExport, type RuntimeEvent, type SandboxMetrics, type Webhook, type WebhookDelivery, type WebhookEventType, } from "./products/observability.js";
28
30
  export { Snapshots, type Snapshot, type SnapshotOptions } from "./snapshots.js";
29
31
  export { Transport, VERSION, apiOrigin, type RequestOptions, type Call } from "./transport.js";
package/dist/login.js CHANGED
@@ -4,7 +4,7 @@ import { hostname } from "node:os";
4
4
  import { promisify } from "node:util";
5
5
  import { me, named } from "./cli-name.js";
6
6
  import { Runtime } from "./client.js";
7
- import { connectionOrigins, connectionStore } from "./credentials.js";
7
+ import { checkBrowserOrigins, connectionOrigins, connectionStore } from "./credentials.js";
8
8
  import { describeRoute, envFetch } from "./proxy.js";
9
9
  class ConnectionError extends Error {
10
10
  status;
@@ -100,7 +100,16 @@ function connection(env, options) {
100
100
  : response.status === 429
101
101
  ? "Too many connection attempts. Try again later."
102
102
  : "Runtime could not complete this connection. Try again.");
103
- return (await response.json());
103
+ try {
104
+ return (await response.json());
105
+ }
106
+ catch (error) {
107
+ // A complete malformed answer remains terminal. A failed body read can
108
+ // hide an issued token, so keep its private key and retry the same request.
109
+ if (error instanceof SyntaxError)
110
+ throw error;
111
+ throw new ConnectionError(503, "Runtime's connection response was interrupted. Retry the connection.");
112
+ }
104
113
  }
105
114
  return { origins, store, request, fetcher };
106
115
  }
@@ -118,7 +127,7 @@ export async function authenticationCommand(args, env, options = {}) {
118
127
  // A pasted key belongs to the website's key list: forget it here, and
119
128
  // revoke it there if it should stop working everywhere.
120
129
  if (saved.connectionId === PASTED) {
121
- await store.remove();
130
+ await store.remove(saved.key);
122
131
  return { disconnected: true };
123
132
  }
124
133
  try {
@@ -128,7 +137,7 @@ export async function authenticationCommand(args, env, options = {}) {
128
137
  if (!(error instanceof ConnectionError && error.status === 401))
129
138
  throw error;
130
139
  }
131
- await store.remove();
140
+ await store.remove(saved.key);
132
141
  return { disconnected: true };
133
142
  }
134
143
  if (command !== "login")
@@ -160,11 +169,7 @@ export async function authenticationCommand(args, env, options = {}) {
160
169
  agentName.length > 80 ||
161
170
  [...agentName].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127))
162
171
  throw new Error("Give the agent a name of up to 80 characters.");
163
- // Never obtain a production key and then direct it at an unrelated API.
164
- const local = (origin) => ["localhost", "127.0.0.1", "[::1]"].includes(new URL(origin).hostname);
165
- if (!((origins.auth === "https://withruntime.com" && origins.api === "https://api.withruntime.com") ||
166
- (local(origins.auth) && local(origins.api))))
167
- throw new Error("Browser login supports Runtime's production endpoints or local test endpoints. Check RUNTIME_AUTH_URL and RUNTIME_API_URL.");
172
+ checkBrowserOrigins(origins);
168
173
  if (withKey) {
169
174
  // The fallback to a browser approval: a key from the website, read from
170
175
  // standard input so it never sits in a command line or shell history.
@@ -200,7 +205,7 @@ export async function authenticationCommand(args, env, options = {}) {
200
205
  catch (error) {
201
206
  if (!(error instanceof ConnectionError && error.status === 401))
202
207
  throw error;
203
- await store.remove();
208
+ await store.remove(saved.key);
204
209
  }
205
210
  }
206
211
  const pending = await beginDeviceLogin(env, agentName, options);
@@ -245,6 +250,7 @@ export async function authenticationCommand(args, env, options = {}) {
245
250
  * the agent, which shows it to the person it works for. */
246
251
  export async function beginDeviceLogin(env, agentName, options = {}) {
247
252
  const { origins, store, request } = connection(env, options);
253
+ checkBrowserOrigins(origins);
248
254
  // A request this machine already started and nobody has answered yet: pick
249
255
  // it up, so a command stopped while it waited (an agent's tool timing out)
250
256
  // shows the same link and code again rather than a new one to approve.
package/dist/mcp.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  /** Bridges an MCP client that speaks stdio to Runtime's hosted MCP endpoint.
2
2
  * Newline-delimited JSON-RPC in, the same out; every message is forwarded as
3
3
  * it is, so the server's tools, schemas and `instructions` reach the client
4
- * unchanged, and nothing here keeps a second catalogue. Starting it costs a
4
+ * unchanged, and nothing here keeps a second catalogue. The one change: the
5
+ * initialize answer's serverInfo.version is this package's. Starting it costs a
5
6
  * process and one TLS connection (undici loads only behind a proxy).
6
7
  *
7
8
  * With no key on this machine it does not fail: it serves one tool,
package/dist/mcp.js CHANGED
@@ -14,7 +14,8 @@ const connectTool = {
14
14
  /** Bridges an MCP client that speaks stdio to Runtime's hosted MCP endpoint.
15
15
  * Newline-delimited JSON-RPC in, the same out; every message is forwarded as
16
16
  * it is, so the server's tools, schemas and `instructions` reach the client
17
- * unchanged, and nothing here keeps a second catalogue. Starting it costs a
17
+ * unchanged, and nothing here keeps a second catalogue. The one change: the
18
+ * initialize answer's serverInfo.version is this package's. Starting it costs a
18
19
  * process and one TLS connection (undici loads only behind a proxy).
19
20
  *
20
21
  * With no key on this machine it does not fail: it serves one tool,
@@ -79,6 +80,10 @@ export async function serveMcp(env = process.env, options = {}) {
79
80
  const result = reply.result;
80
81
  if (result?.protocolVersion)
81
82
  protocolVersion = result.protocolVersion;
83
+ // The client runs this package: it reports the package's version, as
84
+ // before connecting, not the hosted API's contract version (0.2.0).
85
+ if (result?.serverInfo)
86
+ result.serverInfo.version = VERSION;
82
87
  }
83
88
  return { status: response.status, messages, text };
84
89
  }
@@ -149,6 +149,7 @@ export class SandboxService {
149
149
  throw new CompatibilityError("Modal", "fractional CPU reservations");
150
150
  if (params.blockNetwork && (params.outboundDomainAllowlist || params.outboundCidrAllowlist))
151
151
  throw new TypeError("blockNetwork cannot be combined with allowlists");
152
+ const placement = region("Modal", params.regions?.[0]);
152
153
  const built = await image.build(app);
153
154
  let primary;
154
155
  const s = await create(this.runtime, "modal", {
@@ -162,7 +163,7 @@ export class SandboxService {
162
163
  cpu: "reserved",
163
164
  vcpu: params.cpu,
164
165
  memoryMiB: params.memoryMiB,
165
- region: region("Modal", params.regions?.[0]),
166
+ region: placement,
166
167
  timeoutSeconds: Math.ceil((params.timeoutMs ?? 300_000) / 1000),
167
168
  idlePauseSeconds: 0,
168
169
  onLeaseEnd: "stop",