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.
- package/BLAXEL.md +4 -4
- package/DAYTONA.md +52 -40
- package/E2B.md +65 -62
- package/README.md +15 -7
- package/README.src.md +15 -7
- package/VERCEL.md +34 -22
- package/compatibility-lock.json +16 -16
- package/dist/blaxel/errors.js +1 -1
- package/dist/blaxel/lookup.d.ts +3 -2
- package/dist/blaxel/lookup.js +3 -2
- package/dist/blaxel/pagination.d.ts +2 -2
- package/dist/blaxel/pagination.js +45 -17
- package/dist/blaxel/preview.d.ts +2 -1
- package/dist/blaxel/preview.js +15 -13
- package/dist/blaxel/process.js +56 -10
- package/dist/blaxel/sandbox.d.ts +2 -0
- package/dist/blaxel/sandbox.js +2 -0
- package/dist/blaxel/snapshot.js +3 -5
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +262 -56
- package/dist/client.d.ts +41 -3
- package/dist/client.js +67 -11
- package/dist/cloudflare/identity.d.ts +7 -0
- package/dist/cloudflare/identity.js +13 -0
- package/dist/cloudflare/index.d.ts +9 -0
- package/dist/cloudflare/index.js +47 -9
- package/dist/cloudflare/worker.d.ts +4 -0
- package/dist/cloudflare/worker.js +166 -16
- package/dist/codesandbox/guest-service.js +57 -20
- package/dist/codesandbox/index.js +42 -8
- package/dist/compat/core.d.ts +9 -8
- package/dist/compat/core.js +60 -21
- package/dist/compat/files.js +22 -4
- package/dist/compat/lease.d.ts +36 -0
- package/dist/compat/lease.js +87 -0
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +107 -38
- package/dist/daytona/computer-use.d.ts +162 -0
- package/dist/daytona/computer-use.js +324 -0
- package/dist/daytona/context.d.ts +10 -0
- package/dist/daytona/context.js +24 -0
- package/dist/daytona/daytona.d.ts +20 -0
- package/dist/daytona/daytona.js +134 -20
- package/dist/daytona/filesystem.d.ts +17 -4
- package/dist/daytona/filesystem.js +129 -38
- package/dist/daytona/index.d.ts +3 -27
- package/dist/daytona/index.js +2 -7
- package/dist/daytona/process.d.ts +63 -6
- package/dist/daytona/process.js +224 -23
- package/dist/daytona/sandbox.d.ts +20 -6
- package/dist/daytona/sandbox.js +55 -32
- package/dist/e2b/client.d.ts +5 -0
- package/dist/e2b/client.js +6 -2
- package/dist/e2b/commands.d.ts +43 -12
- package/dist/e2b/commands.js +217 -64
- package/dist/e2b/errors.d.ts +7 -0
- package/dist/e2b/errors.js +20 -0
- package/dist/e2b/filesystem.d.ts +2 -1
- package/dist/e2b/filesystem.js +43 -6
- package/dist/e2b/index.d.ts +3 -2
- package/dist/e2b/index.js +2 -1
- package/dist/e2b/pty.d.ts +24 -0
- package/dist/e2b/pty.js +83 -0
- package/dist/e2b/sandbox.d.ts +17 -6
- package/dist/e2b/sandbox.js +239 -43
- package/dist/e2b/users.d.ts +30 -0
- package/dist/e2b/users.js +185 -0
- package/dist/file-source.d.ts +16 -0
- package/dist/file-source.js +162 -0
- package/dist/freestyle/index.d.ts +9 -1
- package/dist/freestyle/index.js +38 -5
- package/dist/index.d.ts +3 -1
- package/dist/login.js +16 -10
- package/dist/mcp.d.ts +2 -1
- package/dist/mcp.js +6 -1
- package/dist/modal/index.js +2 -1
- package/dist/network-cli.js +139 -43
- package/dist/openai-agents/index.js +9 -45
- package/dist/page.d.ts +1 -1
- package/dist/page.js +2 -2
- package/dist/products/browser.d.ts +60 -0
- package/dist/products/browser.js +37 -0
- package/dist/products/desktop.d.ts +2 -2
- package/dist/products/desktop.js +2 -2
- package/dist/products/images.d.ts +3 -1
- package/dist/products/images.js +14 -2
- package/dist/products/index.d.ts +14 -0
- package/dist/products/index.js +12 -0
- package/dist/products/network-products.d.ts +7 -0
- package/dist/products/network-products.js +3 -0
- package/dist/products/observability.d.ts +44 -0
- package/dist/products/observability.js +6 -0
- package/dist/products/previews.d.ts +8 -2
- package/dist/products/previews.js +11 -3
- package/dist/products/private-network.d.ts +22 -0
- package/dist/products/private-network.js +10 -0
- package/dist/products/volumes.d.ts +42 -0
- package/dist/products/volumes.js +36 -0
- package/dist/products/watch.js +61 -8
- package/dist/proxy.js +3 -1
- package/dist/runloop/index.d.ts +13 -27
- package/dist/runloop/index.js +149 -52
- package/dist/sandbox.d.ts +59 -8
- package/dist/sandbox.js +479 -68
- package/dist/snapshots.d.ts +9 -4
- package/dist/sprites/index.js +69 -33
- package/dist/tar.d.ts +3 -1
- package/dist/tar.js +140 -14
- package/dist/transport.d.ts +4 -7
- package/dist/transport.js +205 -122
- package/dist/types.d.ts +5 -2
- package/dist/vercel/command.d.ts +1 -1
- package/dist/vercel/command.js +3 -2
- package/dist/vercel/errors.d.ts +1 -1
- package/dist/vercel/errors.js +7 -1
- package/dist/vercel/index.d.ts +1 -7
- package/dist/vercel/index.js +1 -3
- package/dist/vercel/sandbox-user.d.ts +70 -0
- package/dist/vercel/sandbox-user.js +206 -0
- package/dist/vercel/sandbox.d.ts +55 -20
- package/dist/vercel/sandbox.js +353 -99
- package/dist/vercel/snapshot.d.ts +3 -2
- package/dist/vercel/snapshot.js +9 -5
- 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: (
|
|
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;
|
package/dist/freestyle/index.js
CHANGED
|
@@ -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
|
-
|
|
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 ||
|
|
302
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
}
|
package/dist/modal/index.js
CHANGED
|
@@ -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:
|
|
166
|
+
region: placement,
|
|
166
167
|
timeoutSeconds: Math.ceil((params.timeoutMs ?? 300_000) / 1000),
|
|
167
168
|
idlePauseSeconds: 0,
|
|
168
169
|
onLeaseEnd: "stop",
|