withruntime 0.8.4 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COMPATIBILITY.md +145 -0
- package/E2B.md +23 -13
- package/README.md +8 -5
- package/README.src.md +8 -5
- package/compatibility-lock.json +281 -0
- package/dist/ai-harness/index.js +2 -2
- package/dist/api-defaults.d.ts +17 -0
- package/dist/api-defaults.js +22 -0
- package/dist/blaxel/preview.d.ts +1 -1
- package/dist/blaxel/preview.js +1 -1
- package/dist/blaxel/process.d.ts +1 -1
- package/dist/blaxel/process.js +11 -2
- package/dist/cli-words.d.ts +27 -0
- package/dist/cli-words.js +135 -0
- package/dist/cli.js +162 -31
- package/dist/client.d.ts +4 -1
- package/dist/client.js +8 -2
- package/dist/cloudflare/index.d.ts +294 -0
- package/dist/cloudflare/index.js +554 -0
- package/dist/cloudflare/worker.d.ts +415 -0
- package/dist/cloudflare/worker.js +297 -0
- package/dist/codesandbox/guest-service.d.ts +31 -0
- package/dist/codesandbox/guest-service.js +453 -0
- package/dist/codesandbox/index.d.ts +386 -0
- package/dist/codesandbox/index.js +1313 -0
- package/dist/compat/core.d.ts +39 -0
- package/dist/compat/core.js +199 -0
- package/dist/compat/files.d.ts +26 -0
- package/dist/compat/files.js +115 -0
- package/dist/compat/shell.d.ts +4 -0
- package/dist/compat/shell.js +165 -0
- package/dist/daytona/context.js +2 -1
- package/dist/daytona/process.d.ts +1 -1
- package/dist/daytona/process.js +27 -4
- package/dist/e2b/code-interpreter.d.ts +4 -5
- package/dist/e2b/code-interpreter.js +54 -56
- package/dist/e2b/commands.d.ts +5 -4
- package/dist/e2b/commands.js +37 -29
- package/dist/e2b/errors.js +1 -1
- package/dist/e2b/filesystem.d.ts +8 -6
- package/dist/e2b/filesystem.js +127 -28
- package/dist/e2b/sandbox.d.ts +4 -3
- package/dist/e2b/sandbox.js +10 -10
- package/dist/errors.d.ts +8 -0
- package/dist/errors.js +15 -2
- package/dist/freestyle/index.d.ts +301 -0
- package/dist/freestyle/index.js +310 -0
- package/dist/index.d.ts +1 -0
- package/dist/mcp.js +2 -2
- package/dist/modal/index.d.ts +205 -0
- package/dist/modal/index.js +485 -0
- package/dist/network-cli.js +1 -1
- package/dist/openai-agents/index.js +9 -26
- package/dist/products/audit.d.ts +3 -0
- package/dist/products/index.d.ts +2 -0
- package/dist/products/index.js +2 -0
- package/dist/products/interpreter.d.ts +11 -4
- package/dist/products/interpreter.js +33 -9
- package/dist/products/jobs.js +3 -2
- package/dist/products/limits.d.ts +10 -0
- package/dist/products/previews.d.ts +10 -2
- package/dist/products/previews.js +3 -2
- package/dist/products/tailscale.d.ts +57 -0
- package/dist/products/tailscale.js +17 -0
- package/dist/products/watch.d.ts +25 -4
- package/dist/products/watch.js +75 -14
- package/dist/runloop/index.d.ts +340 -0
- package/dist/runloop/index.js +438 -0
- package/dist/runloop/pagination.d.ts +46 -0
- package/dist/runloop/pagination.js +82 -0
- package/dist/runloop/snapshots.d.ts +52 -0
- package/dist/runloop/snapshots.js +244 -0
- package/dist/sandbox.d.ts +40 -9
- package/dist/sandbox.js +273 -76
- package/dist/snapshots.d.ts +11 -1
- package/dist/snapshots.js +11 -1
- package/dist/sprites/index.d.ts +251 -0
- package/dist/sprites/index.js +552 -0
- package/dist/tar.d.ts +8 -1
- package/dist/tar.js +185 -35
- package/dist/tools/index.js +2 -1
- package/dist/transport.d.ts +20 -2
- package/dist/transport.js +48 -11
- package/dist/types.d.ts +52 -3
- package/dist/vercel/command.js +12 -1
- package/dist/vercel/errors.d.ts +2 -2
- package/dist/vercel/errors.js +12 -1
- package/dist/vercel/filesystem.d.ts +21 -18
- package/dist/vercel/filesystem.js +95 -48
- package/dist/vercel/sandbox.js +5 -5
- package/package.json +45 -3
package/dist/tar.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { chmod, lstat, mkdir, readdir, readFile, realpath, symlink, unlink,
|
|
1
|
+
import { chmod, lstat, mkdir, mkdtemp, open, readdir, readFile, realpath, rename, rm, symlink, unlink, } from "node:fs/promises";
|
|
2
2
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { Readable } from "node:stream";
|
|
4
|
+
import { createGunzip, gzipSync } from "node:zlib";
|
|
5
|
+
import { RuntimeError } from "./errors.js";
|
|
4
6
|
/* A small ustar writer and reader for directory uploads and downloads, so the
|
|
5
7
|
SDK needs no dependency. Regular files, directories and symbolic links;
|
|
6
8
|
names up to 255 bytes through the ustar prefix field. */
|
|
@@ -111,34 +113,119 @@ async function linkStaysInside(root, directory, link) {
|
|
|
111
113
|
}
|
|
112
114
|
return true;
|
|
113
115
|
}
|
|
114
|
-
/**
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
116
|
+
/** A folder that did not arrive whole: tar in the sandbox stopped part way
|
|
117
|
+
* (a file it may not read, one that changed as it was read), which ends the
|
|
118
|
+
* gzip stream short, or the connection was lost (ARCHITECTURE.md section 10,
|
|
119
|
+
* "Folders over HTTP"). */
|
|
120
|
+
function cutShort(cause) {
|
|
121
|
+
return new RuntimeError({
|
|
122
|
+
message: "The folder's archive arrived cut short: tar in the sandbox stopped part way, or the connection was lost. Nothing was written.",
|
|
123
|
+
hint: "Try again. If it fails the same way, a file in the folder cannot be read by the sandbox user or changes as it is read.",
|
|
124
|
+
code: "download_incomplete",
|
|
125
|
+
status: 0,
|
|
126
|
+
...(cause === undefined ? {} : { cause }),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/** The gzip stream's bytes, decompressed as they arrive. */
|
|
130
|
+
function gunzipped(source) {
|
|
131
|
+
const input = Readable.from(source, { objectMode: false });
|
|
132
|
+
const output = createGunzip();
|
|
133
|
+
input.on("error", (error) => output.destroy(error));
|
|
134
|
+
input.pipe(output);
|
|
135
|
+
output.on("close", () => input.destroy());
|
|
136
|
+
return output;
|
|
137
|
+
}
|
|
138
|
+
/** Exact byte counts from a stream of chunks, holding at most one chunk. */
|
|
139
|
+
class Bytes {
|
|
140
|
+
#chunks;
|
|
141
|
+
#held = new Uint8Array(0);
|
|
142
|
+
constructor(source) {
|
|
143
|
+
this.#chunks = source[Symbol.asyncIterator]();
|
|
144
|
+
}
|
|
145
|
+
async #more() {
|
|
146
|
+
let next;
|
|
147
|
+
try {
|
|
148
|
+
next = await this.#chunks.next();
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
throw error instanceof RuntimeError ? error : cutShort(error);
|
|
152
|
+
}
|
|
153
|
+
if (next.done)
|
|
154
|
+
return false;
|
|
155
|
+
this.#held = this.#held.length ? Buffer.concat([this.#held, next.value]) : next.value;
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
/** `n` bytes, or undefined at a clean end before any of them. */
|
|
159
|
+
async exactly(n) {
|
|
160
|
+
while (this.#held.length < n) {
|
|
161
|
+
if (await this.#more())
|
|
162
|
+
continue;
|
|
163
|
+
if (this.#held.length === 0)
|
|
164
|
+
return undefined;
|
|
165
|
+
throw cutShort();
|
|
166
|
+
}
|
|
167
|
+
const out = this.#held.subarray(0, n);
|
|
168
|
+
this.#held = this.#held.subarray(n);
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
/** The next `n` bytes as they arrive. */
|
|
172
|
+
async *take(n) {
|
|
173
|
+
while (n > 0) {
|
|
174
|
+
if (this.#held.length === 0 && !(await this.#more()))
|
|
175
|
+
throw cutShort();
|
|
176
|
+
const out = this.#held.subarray(0, n);
|
|
177
|
+
this.#held = this.#held.subarray(out.length);
|
|
178
|
+
n -= out.length;
|
|
179
|
+
yield out;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/** Reads to the end, so the gzip stream's own check has run. */
|
|
183
|
+
async drain() {
|
|
184
|
+
this.#held = new Uint8Array(0);
|
|
185
|
+
while (await this.#more())
|
|
186
|
+
this.#held = new Uint8Array(0);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/** Unpacks a tar stream into `root`, a fresh folder: files and folders as they
|
|
190
|
+
* arrive, links noted to make at the end. Throws unless the archive's end
|
|
191
|
+
* arrived. */
|
|
192
|
+
async function unpackEntries(bytes, root) {
|
|
193
|
+
const links = [];
|
|
194
|
+
const decode = (slice) => {
|
|
121
195
|
const end = slice.indexOf(0);
|
|
122
196
|
return new TextDecoder().decode(end < 0 ? slice : slice.subarray(0, end));
|
|
123
197
|
};
|
|
124
|
-
const links = [];
|
|
125
198
|
let longName;
|
|
126
199
|
let longLink;
|
|
127
|
-
for (
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
200
|
+
for (;;) {
|
|
201
|
+
const header = await bytes.exactly(512);
|
|
202
|
+
if (!header)
|
|
203
|
+
throw cutShort();
|
|
204
|
+
if (header.every((byte) => byte === 0)) {
|
|
205
|
+
await bytes.drain();
|
|
206
|
+
return links;
|
|
207
|
+
}
|
|
208
|
+
const text = (start, length) => decode(header.subarray(start, start + length));
|
|
209
|
+
const size = parseInt(text(124, 12).trim() || "0", 8);
|
|
210
|
+
const type = String.fromCharCode(header[156] ?? 48);
|
|
211
|
+
const prefix = text(345, 155);
|
|
212
|
+
let name = longName ?? (prefix ? `${prefix}/${text(0, 100)}` : text(0, 100));
|
|
213
|
+
const link = longLink ?? text(157, 100);
|
|
135
214
|
longName = undefined;
|
|
136
215
|
longLink = undefined;
|
|
137
|
-
const mode = parseInt(text(
|
|
138
|
-
const
|
|
139
|
-
|
|
216
|
+
const mode = parseInt(text(100, 8).trim() || "644", 8);
|
|
217
|
+
const padding = Math.ceil(size / 512) * 512 - size;
|
|
218
|
+
const skip = async () => {
|
|
219
|
+
for await (const _ of bytes.take(size + padding))
|
|
220
|
+
;
|
|
221
|
+
};
|
|
140
222
|
if (type === "L" || type === "K") {
|
|
141
|
-
const
|
|
223
|
+
const parts = [];
|
|
224
|
+
for await (const part of bytes.take(size))
|
|
225
|
+
parts.push(Uint8Array.from(part));
|
|
226
|
+
for await (const _ of bytes.take(padding))
|
|
227
|
+
;
|
|
228
|
+
const value = new TextDecoder().decode(Buffer.concat(parts)).replace(/\0.*$/s, "");
|
|
142
229
|
if (type === "L")
|
|
143
230
|
longName = value;
|
|
144
231
|
else
|
|
@@ -146,29 +233,92 @@ export async function unpackArchive(archive, target) {
|
|
|
146
233
|
continue;
|
|
147
234
|
}
|
|
148
235
|
name = name.replace(/^\.\//, "");
|
|
149
|
-
if (!name || name === "." || name === "./")
|
|
236
|
+
if (!name || name === "." || name === "./") {
|
|
237
|
+
await skip();
|
|
150
238
|
continue;
|
|
239
|
+
}
|
|
151
240
|
const destination = resolve(root, name);
|
|
152
241
|
await assertPlain(root, destination, name);
|
|
153
|
-
if (type === "5")
|
|
242
|
+
if (type === "5") {
|
|
154
243
|
await mkdir(destination, { recursive: true });
|
|
155
|
-
|
|
244
|
+
await skip();
|
|
245
|
+
}
|
|
246
|
+
else if (type === "2") {
|
|
156
247
|
links.push({ destination, link, name });
|
|
248
|
+
await skip();
|
|
249
|
+
}
|
|
157
250
|
else if (type === "0" || type === "\0" || type === "7") {
|
|
158
251
|
await mkdir(dirname(destination), { recursive: true });
|
|
159
|
-
await
|
|
252
|
+
const file = await open(destination, "w");
|
|
253
|
+
try {
|
|
254
|
+
for await (const part of bytes.take(size))
|
|
255
|
+
await file.write(part);
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
await file.close();
|
|
259
|
+
}
|
|
160
260
|
await chmod(destination, mode & 0o777);
|
|
261
|
+
for await (const _ of bytes.take(padding))
|
|
262
|
+
;
|
|
161
263
|
}
|
|
264
|
+
else
|
|
265
|
+
await skip();
|
|
162
266
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
267
|
+
}
|
|
268
|
+
/** Moves what was unpacked in `from` into `to`, merging with what is there
|
|
269
|
+
* and replacing files of the same name, never through a link in `to`. */
|
|
270
|
+
async function merge(from, to, root) {
|
|
271
|
+
for (const name of await readdir(from)) {
|
|
272
|
+
const source = join(from, name);
|
|
273
|
+
const destination = join(to, name);
|
|
274
|
+
await assertPlain(root, destination, relative(root, destination));
|
|
275
|
+
const [kind, there] = await Promise.all([
|
|
276
|
+
lstat(source),
|
|
277
|
+
lstat(destination).catch(() => undefined),
|
|
278
|
+
]);
|
|
279
|
+
if (kind.isDirectory() && there?.isDirectory())
|
|
280
|
+
await merge(source, destination, root);
|
|
281
|
+
else
|
|
282
|
+
await rename(source, destination);
|
|
167
283
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
284
|
+
}
|
|
285
|
+
/** Unpacks a gzipped tar that arrives as a stream into `target`, refusing any
|
|
286
|
+
* entry that would land outside it, holding no more than a chunk at a time.
|
|
287
|
+
* It unpacks into a folder beside the target and moves it into place only
|
|
288
|
+
* once the whole archive arrived, so an archive cut short leaves nothing
|
|
289
|
+
* behind that could pass for the folder. A target that exists is merged
|
|
290
|
+
* into, files of the same name replaced. */
|
|
291
|
+
export async function unpackStream(source, target) {
|
|
292
|
+
const final = resolve(target);
|
|
293
|
+
await mkdir(dirname(final), { recursive: true });
|
|
294
|
+
const staging = await mkdtemp(`${final}.runtime-partial-`);
|
|
295
|
+
const stream = gunzipped(source);
|
|
296
|
+
try {
|
|
297
|
+
const root = await realpath(staging);
|
|
298
|
+
const links = await unpackEntries(new Bytes(stream), root);
|
|
299
|
+
for (const { destination, link, name } of links) {
|
|
300
|
+
await assertPlain(root, destination, name);
|
|
301
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
302
|
+
await symlink(link, destination).catch(() => undefined);
|
|
303
|
+
}
|
|
304
|
+
for (const { destination, link, name } of links) {
|
|
305
|
+
if (await linkStaysInside(root, dirname(destination), link))
|
|
306
|
+
continue;
|
|
307
|
+
await unlink(destination).catch(() => undefined);
|
|
308
|
+
throw new Error(`Refusing an archive link that leads outside the target: ${name} -> ${link}`);
|
|
309
|
+
}
|
|
310
|
+
const there = await lstat(final).catch(() => undefined);
|
|
311
|
+
if (!there)
|
|
312
|
+
await rename(staging, final);
|
|
313
|
+
else
|
|
314
|
+
await merge(root, await realpath(final), await realpath(final));
|
|
315
|
+
}
|
|
316
|
+
finally {
|
|
317
|
+
stream.destroy();
|
|
318
|
+
await rm(staging, { recursive: true, force: true });
|
|
173
319
|
}
|
|
174
320
|
}
|
|
321
|
+
/** Unpacks a whole gzipped tar held in memory, by the same rules. */
|
|
322
|
+
export function unpackArchive(archive, target) {
|
|
323
|
+
return unpackStream([archive], target);
|
|
324
|
+
}
|
package/dist/tools/index.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* chooses commands and paths, never the sandbox, the account or the key.
|
|
11
11
|
* Relative paths are under /workspace, and output is capped so one noisy
|
|
12
12
|
* command cannot flood the model's context. */
|
|
13
|
+
import { TOOL_EXEC_TIMEOUT_SECONDS } from "../api-defaults.js";
|
|
13
14
|
import { NotFoundError } from "../errors.js";
|
|
14
15
|
const WORKSPACE = "/workspace";
|
|
15
16
|
/** A POSIX path joined and normalized: `..` cannot climb past `/`. */
|
|
@@ -34,7 +35,7 @@ export function clip(text, limit) {
|
|
|
34
35
|
/** runtime_exec, runtime_read_file, runtime_write_file and runtime_list_files, bound to `sandbox`. */
|
|
35
36
|
export function sandboxTools(sandbox, options = {}) {
|
|
36
37
|
const root = resolvePath("/", options.root ?? WORKSPACE);
|
|
37
|
-
const timeoutSeconds = options.timeoutSeconds ??
|
|
38
|
+
const timeoutSeconds = options.timeoutSeconds ?? TOOL_EXEC_TIMEOUT_SECONDS;
|
|
38
39
|
const limit = options.maxOutputChars ?? 20_000;
|
|
39
40
|
const inFilesApi = (path) => path.startsWith(`${WORKSPACE}/`);
|
|
40
41
|
return [
|
package/dist/transport.d.ts
CHANGED
|
@@ -1,17 +1,35 @@
|
|
|
1
1
|
import { RuntimeError } from "./errors.js";
|
|
2
|
-
export declare const VERSION = "0.
|
|
2
|
+
export declare const VERSION = "0.9.0";
|
|
3
3
|
export declare const DEFAULT_BASE_URL = "https://api.withruntime.com";
|
|
4
|
+
/** Runtime's API as code inside a Runtime sandbox reaches it: the sandbox's
|
|
5
|
+
* own host sends each request on to DEFAULT_BASE_URL over HTTPS. The API runs
|
|
6
|
+
* on that host, whose addresses a sandbox cannot reach directly. Plain HTTP
|
|
7
|
+
* because the hop never leaves the machine: it goes from the program to the
|
|
8
|
+
* guest's own proxy and over the sandbox's private channel to its host. */
|
|
9
|
+
export declare const SANDBOX_BASE_URL = "http://runtime.internal";
|
|
10
|
+
/** True in a Runtime sandbox. Node and Bun only; a browser is never one. */
|
|
11
|
+
export declare function inRuntimeSandbox(marker?: string): boolean;
|
|
12
|
+
/** Where calls go when no base URL is given: RUNTIME_API_URL, then Runtime's
|
|
13
|
+
* public API, which `reachable` turns into runtime.internal in a sandbox. */
|
|
14
|
+
export declare function defaultBaseUrl(env?: Record<string, string | undefined>, inSandbox?: typeof inRuntimeSandbox): string;
|
|
15
|
+
/** The origin calls for `origin` are sent to from here. In a sandbox the
|
|
16
|
+
* public API is its own host, which it cannot reach directly, so calls for it
|
|
17
|
+
* go to runtime.internal; every other origin is left as it is. */
|
|
18
|
+
export declare function reachable(origin: string, inSandbox?: typeof inRuntimeSandbox): string;
|
|
4
19
|
export type RequestOptions = {
|
|
5
20
|
/** Sent as Idempotency-Key. Omit it: the SDK makes one per call and keeps
|
|
6
21
|
* it across its own retries, so a retried write never happens twice. Pass
|
|
7
22
|
* your own only to retry a call yourself after the process restarted. */
|
|
8
23
|
idempotencyKey?: string;
|
|
9
24
|
signal?: AbortSignal;
|
|
10
|
-
/** Deadline for this call, retries included.
|
|
25
|
+
/** Deadline for this call, retries included. 0 disables this deadline;
|
|
26
|
+
* an explicit signal can still cancel the call. */
|
|
11
27
|
timeoutMs?: number;
|
|
12
28
|
};
|
|
13
29
|
export type Query = Record<string, string | number | boolean | readonly string[] | undefined>;
|
|
14
30
|
export type Call = RequestOptions & {
|
|
31
|
+
/** Internal protocol hook: successful headers arrived, before reading the body. */
|
|
32
|
+
onResponse?: (response: Response) => void;
|
|
15
33
|
method: "GET" | "POST" | "PUT" | "DELETE";
|
|
16
34
|
path: string;
|
|
17
35
|
query?: Query;
|
package/dist/transport.js
CHANGED
|
@@ -1,17 +1,52 @@
|
|
|
1
|
-
import { ConnectionError, errorFor, RuntimeError, WAITS_FOR_ROOM } from "./errors.js";
|
|
1
|
+
import { ConnectionError, DELIBERATE, errorFor, RuntimeError, WAITS_FOR_ROOM } from "./errors.js";
|
|
2
2
|
import { describeRoute, envFetch, openWebSocket } from "./proxy.js";
|
|
3
|
-
export const VERSION = "0.
|
|
3
|
+
export const VERSION = "0.9.0";
|
|
4
4
|
export const DEFAULT_BASE_URL = "https://api.withruntime.com";
|
|
5
|
+
/** Runtime's API as code inside a Runtime sandbox reaches it: the sandbox's
|
|
6
|
+
* own host sends each request on to DEFAULT_BASE_URL over HTTPS. The API runs
|
|
7
|
+
* on that host, whose addresses a sandbox cannot reach directly. Plain HTTP
|
|
8
|
+
* because the hop never leaves the machine: it goes from the program to the
|
|
9
|
+
* guest's own proxy and over the sandbox's private channel to its host. */
|
|
10
|
+
export const SANDBOX_BASE_URL = "http://runtime.internal";
|
|
11
|
+
/** Every Runtime sandbox has this file; guest-net.py keeps it current. */
|
|
12
|
+
const SANDBOX_MARKER = "/run/runtime/environment.json";
|
|
13
|
+
/** True in a Runtime sandbox. Node and Bun only; a browser is never one. */
|
|
14
|
+
export function inRuntimeSandbox(marker = SANDBOX_MARKER) {
|
|
15
|
+
try {
|
|
16
|
+
const host = globalThis.process;
|
|
17
|
+
return host?.getBuiltinModule?.("node:fs")?.existsSync?.(marker) === true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Where calls go when no base URL is given: RUNTIME_API_URL, then Runtime's
|
|
24
|
+
* public API, which `reachable` turns into runtime.internal in a sandbox. */
|
|
25
|
+
export function defaultBaseUrl(env = processEnv(), inSandbox = inRuntimeSandbox) {
|
|
26
|
+
return reachable(env.RUNTIME_API_URL || DEFAULT_BASE_URL, inSandbox);
|
|
27
|
+
}
|
|
28
|
+
/** The origin calls for `origin` are sent to from here. In a sandbox the
|
|
29
|
+
* public API is its own host, which it cannot reach directly, so calls for it
|
|
30
|
+
* go to runtime.internal; every other origin is left as it is. */
|
|
31
|
+
export function reachable(origin, inSandbox = inRuntimeSandbox) {
|
|
32
|
+
return apiOrigin(origin) === DEFAULT_BASE_URL && inSandbox() ? SANDBOX_BASE_URL : origin;
|
|
33
|
+
}
|
|
34
|
+
function processEnv() {
|
|
35
|
+
return (globalThis.process?.env ?? {});
|
|
36
|
+
}
|
|
5
37
|
export function apiOrigin(value) {
|
|
6
38
|
const url = new URL(value);
|
|
7
|
-
|
|
39
|
+
// runtime.internal is reserved and never resolves outside a sandbox, so a
|
|
40
|
+
// key sent there in plain HTTP never leaves the sandbox's host.
|
|
41
|
+
const local = ["localhost", "127.0.0.1", "[::1]", "runtime.internal"].includes(url.hostname);
|
|
8
42
|
if ((url.protocol !== "https:" && !(local && url.protocol === "http:")) ||
|
|
43
|
+
(url.hostname === "runtime.internal" && (url.protocol !== "http:" || url.port !== "")) ||
|
|
9
44
|
url.username ||
|
|
10
45
|
url.password ||
|
|
11
46
|
url.search ||
|
|
12
47
|
url.hash ||
|
|
13
48
|
url.pathname !== "/")
|
|
14
|
-
throw new Error("Use an HTTPS API origin (or http://localhost for tests).");
|
|
49
|
+
throw new Error("Use an HTTPS API origin (or http://runtime.internal inside a sandbox, http://localhost for tests).");
|
|
15
50
|
return url.origin;
|
|
16
51
|
}
|
|
17
52
|
export function encodeQuery(query) {
|
|
@@ -30,10 +65,6 @@ export function encodeQuery(query) {
|
|
|
30
65
|
const text = params.toString();
|
|
31
66
|
return text ? `?${text}` : "";
|
|
32
67
|
}
|
|
33
|
-
/** 503s that are a deliberate state, not a passing one: a product switched
|
|
34
|
-
* off here, or paused on purpose. Retrying cannot change them, so they fail
|
|
35
|
-
* at once with their own words. host_unavailable and busy are still retried. */
|
|
36
|
-
const DELIBERATE = new Set(["fork_unavailable", "previews_unavailable", "unavailable"]);
|
|
37
68
|
const sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
38
69
|
const timer = setTimeout(done, ms);
|
|
39
70
|
function done() {
|
|
@@ -99,7 +130,7 @@ export class Transport {
|
|
|
99
130
|
throw missingKey();
|
|
100
131
|
else
|
|
101
132
|
this.#apiKey = options.apiKey;
|
|
102
|
-
this.baseUrl = apiOrigin(options.baseUrl
|
|
133
|
+
this.baseUrl = apiOrigin(options.baseUrl ? reachable(options.baseUrl) : defaultBaseUrl());
|
|
103
134
|
// The environment's proxy (HTTPS_PROXY, NO_PROXY) on Node and Bun alike;
|
|
104
135
|
// a fetch passed in is used as it is.
|
|
105
136
|
this.#fetch = options.fetch ?? envFetch;
|
|
@@ -147,7 +178,8 @@ export class Transport {
|
|
|
147
178
|
const roomMs = Math.max(0, call.waitForCapacityMs ?? 0);
|
|
148
179
|
const roomUntil = performance.now() + roomMs;
|
|
149
180
|
let roomAttempt = 0;
|
|
150
|
-
const
|
|
181
|
+
const timeoutMs = call.timeoutMs ?? this.#timeoutMs;
|
|
182
|
+
const deadline = timeoutMs === 0 ? new AbortController().signal : AbortSignal.timeout(timeoutMs + roomMs);
|
|
151
183
|
const signal = call.signal ? AbortSignal.any([call.signal, deadline]) : deadline;
|
|
152
184
|
const url = `${this.baseUrl}${call.path}${encodeQuery(call.query)}`;
|
|
153
185
|
const body = call.bytes ?? (call.body === undefined ? undefined : JSON.stringify(call.body));
|
|
@@ -211,8 +243,10 @@ export class Transport {
|
|
|
211
243
|
return new Response(text, { headers: response.headers });
|
|
212
244
|
late = { status: failed, text };
|
|
213
245
|
}
|
|
214
|
-
else if (response.ok)
|
|
246
|
+
else if (response.ok) {
|
|
247
|
+
call.onResponse?.(response);
|
|
215
248
|
return response;
|
|
249
|
+
}
|
|
216
250
|
const status = late?.status ?? response.status;
|
|
217
251
|
// Runtime never answers 407: it is a proxy refusing the tunnel, which
|
|
218
252
|
// Bun's fetch hands back as a response rather than an error.
|
|
@@ -336,12 +370,14 @@ export class Transport {
|
|
|
336
370
|
let buffer = "";
|
|
337
371
|
try {
|
|
338
372
|
for (;;) {
|
|
373
|
+
call.signal?.throwIfAborted();
|
|
339
374
|
const { value, done } = await reader.read();
|
|
340
375
|
if (done)
|
|
341
376
|
break;
|
|
342
377
|
buffer += value;
|
|
343
378
|
let newline = buffer.indexOf("\n");
|
|
344
379
|
while (newline >= 0) {
|
|
380
|
+
call.signal?.throwIfAborted();
|
|
345
381
|
const line = buffer.slice(0, newline).trim();
|
|
346
382
|
buffer = buffer.slice(newline + 1);
|
|
347
383
|
if (line)
|
|
@@ -349,6 +385,7 @@ export class Transport {
|
|
|
349
385
|
newline = buffer.indexOf("\n");
|
|
350
386
|
}
|
|
351
387
|
}
|
|
388
|
+
call.signal?.throwIfAborted();
|
|
352
389
|
if (buffer.trim())
|
|
353
390
|
yield JSON.parse(buffer);
|
|
354
391
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -39,8 +39,14 @@ export type SandboxInfo = {
|
|
|
39
39
|
heldMicros: number;
|
|
40
40
|
simulated?: boolean;
|
|
41
41
|
replayed?: boolean;
|
|
42
|
+
/** The names of its own environment variables (`env`), on a create, an
|
|
43
|
+
* update and a read of one sandbox. Values are never shown. */
|
|
44
|
+
envNames?: string[];
|
|
42
45
|
/** On a create from an image with a start command: whether it started,
|
|
43
|
-
* became ready (readyMs), timed out, or exited first.
|
|
46
|
+
* became ready (readyMs), timed out, or exited first. The create's answer
|
|
47
|
+
* alone carries it, and the Sandbox that create returned keeps it through
|
|
48
|
+
* later reads; `sandboxes.get` and `list` have none, because the API keeps
|
|
49
|
+
* no record of it. */
|
|
44
50
|
start?: {
|
|
45
51
|
state: "started" | "ready" | "timeout" | "exited" | "not_running";
|
|
46
52
|
processId?: string;
|
|
@@ -54,6 +60,11 @@ export type SandboxInfo = {
|
|
|
54
60
|
export type CreateSandbox = {
|
|
55
61
|
name?: string;
|
|
56
62
|
labels?: Record<string, string>;
|
|
63
|
+
/** Environment variables for every command, process, terminal, SSH session
|
|
64
|
+
* and the image's start command in this sandbox, under each command's own
|
|
65
|
+
* `env`. At most 32 variables and 32 KiB. Values are never shown again;
|
|
66
|
+
* answers list only `envNames`. Copies made by fork() keep them. */
|
|
67
|
+
env?: Record<string, string>;
|
|
57
68
|
/** Omit to use the free trial while it lasts, then prepaid credit. */
|
|
58
69
|
funding?: "trial" | "paid";
|
|
59
70
|
region?: string;
|
|
@@ -103,6 +114,13 @@ export type CreateSandbox = {
|
|
|
103
114
|
path: string;
|
|
104
115
|
mode?: "rw" | "snapshot";
|
|
105
116
|
}[];
|
|
117
|
+
/** Paid only: join your Tailscale network once running. `authKeySecret`
|
|
118
|
+
* names a job secret holding the auth key. */
|
|
119
|
+
tailscale?: {
|
|
120
|
+
authKeySecret: string;
|
|
121
|
+
hostname?: string;
|
|
122
|
+
tags?: string[];
|
|
123
|
+
};
|
|
106
124
|
};
|
|
107
125
|
export type CommandResult = {
|
|
108
126
|
exitCode: number | null;
|
|
@@ -124,8 +142,8 @@ export type ExecOptions = {
|
|
|
124
142
|
/** Default 60 000, or 24 hours when the output streams (onStdout, onStderr or
|
|
125
143
|
* execStream); up to 24 hours. A timeout is a result (timedOut), not an error. */
|
|
126
144
|
timeoutMs?: number;
|
|
127
|
-
onStdout?: (text: string) =>
|
|
128
|
-
onStderr?: (text: string) =>
|
|
145
|
+
onStdout?: (text: string) => unknown;
|
|
146
|
+
onStderr?: (text: string) => unknown;
|
|
129
147
|
/** Throw CommandError when the exit code is not 0. */
|
|
130
148
|
check?: boolean;
|
|
131
149
|
signal?: AbortSignal;
|
|
@@ -141,6 +159,7 @@ export type ProcessInfo = {
|
|
|
141
159
|
pty: boolean;
|
|
142
160
|
stdinOpen: boolean;
|
|
143
161
|
stdinOffset: number;
|
|
162
|
+
outputEncoding?: "utf8" | "base64";
|
|
144
163
|
startedAt: string;
|
|
145
164
|
endedAt: string | null;
|
|
146
165
|
timeoutMs: number | null;
|
|
@@ -155,6 +174,7 @@ export type OutputEvent = {
|
|
|
155
174
|
type: "stdout" | "stderr";
|
|
156
175
|
data: string;
|
|
157
176
|
offset: number;
|
|
177
|
+
base64?: string;
|
|
158
178
|
} | {
|
|
159
179
|
type: "exit";
|
|
160
180
|
exitCode: number | null;
|
|
@@ -177,6 +197,13 @@ export type OutputEvent = {
|
|
|
177
197
|
requestId?: string;
|
|
178
198
|
};
|
|
179
199
|
};
|
|
200
|
+
export type BinaryOutputEvent = Exclude<OutputEvent, {
|
|
201
|
+
type: "stdout" | "stderr";
|
|
202
|
+
}> | {
|
|
203
|
+
type: "stdout" | "stderr";
|
|
204
|
+
data: Uint8Array;
|
|
205
|
+
offset: number;
|
|
206
|
+
};
|
|
180
207
|
export type FileEntry = {
|
|
181
208
|
name: string;
|
|
182
209
|
path: string;
|
|
@@ -184,6 +211,11 @@ export type FileEntry = {
|
|
|
184
211
|
size: number;
|
|
185
212
|
mode: string;
|
|
186
213
|
modifiedAt: string;
|
|
214
|
+
uid?: number;
|
|
215
|
+
gid?: number;
|
|
216
|
+
owner?: string;
|
|
217
|
+
group?: string;
|
|
218
|
+
symlinkTarget?: string;
|
|
187
219
|
};
|
|
188
220
|
/** GET /v1/usage. Money is integer microdollars in strings (1,000,000 = $1),
|
|
189
221
|
* exact past 2^53: available = credited - spent - expired - held. */
|
|
@@ -226,6 +258,9 @@ export type FeedbackKind = "bug" | "missing_feature" | "competitor_gap" | "migra
|
|
|
226
258
|
export type SandboxSettings = {
|
|
227
259
|
name?: string;
|
|
228
260
|
labels?: Record<string, string>;
|
|
261
|
+
/** Changes its environment: a value sets a variable, null removes it, and
|
|
262
|
+
* the rest stay. Commands started afterwards get the change. */
|
|
263
|
+
env?: Record<string, string | null>;
|
|
229
264
|
/** A request to a paused sandbox wakes it. */
|
|
230
265
|
autoWake?: boolean;
|
|
231
266
|
/** Pause after this many seconds with no activity, counted from now; 0
|
|
@@ -236,6 +271,20 @@ export type SandboxSettings = {
|
|
|
236
271
|
/** Lifetime cap in microdollars; null removes it. */
|
|
237
272
|
maxTotalCostMicros?: number | null;
|
|
238
273
|
};
|
|
274
|
+
/** A deleted sandbox, as `delete()` answers it the first time and every time
|
|
275
|
+
* after. It reads nowhere else afterwards. */
|
|
276
|
+
export type DeletedSandbox = {
|
|
277
|
+
id: string;
|
|
278
|
+
kind: "sandbox";
|
|
279
|
+
name: string | null;
|
|
280
|
+
labels: Record<string, string>;
|
|
281
|
+
status: "deleted";
|
|
282
|
+
/** "stopping" until its machine has stopped, usually well under a second. */
|
|
283
|
+
state: string;
|
|
284
|
+
deletedAt: string;
|
|
285
|
+
endedAt: string | null;
|
|
286
|
+
replayed?: boolean;
|
|
287
|
+
};
|
|
239
288
|
/** How `sandbox.keepAlive()` extends the lease. */
|
|
240
289
|
export type KeepAliveOptions = {
|
|
241
290
|
/** How often it checks, in seconds. Default 60. */
|
package/dist/vercel/command.js
CHANGED
|
@@ -47,18 +47,25 @@ export class Command {
|
|
|
47
47
|
logs(opts = {}) {
|
|
48
48
|
const abort = new AbortController();
|
|
49
49
|
const forward = () => abort.abort();
|
|
50
|
-
opts.signal?.
|
|
50
|
+
if (opts.signal?.aborted)
|
|
51
|
+
abort.abort();
|
|
52
|
+
else
|
|
53
|
+
opts.signal?.addEventListener("abort", forward, { once: true });
|
|
51
54
|
const lines = this.init.lines;
|
|
52
55
|
const process = this.init.process;
|
|
53
56
|
const sandboxName = this.init.sandboxName;
|
|
54
57
|
async function* read() {
|
|
55
58
|
try {
|
|
59
|
+
if (abort.signal.aborted)
|
|
60
|
+
return;
|
|
56
61
|
if (lines) {
|
|
57
62
|
yield* lines;
|
|
58
63
|
return;
|
|
59
64
|
}
|
|
60
65
|
try {
|
|
61
66
|
for await (const event of process.output({ signal: abort.signal })) {
|
|
67
|
+
if (event.type === "truncated")
|
|
68
|
+
throw new StreamError("output_truncated", "Some command output is no longer available.", process.id);
|
|
62
69
|
if (event.type === "stdout" || event.type === "stderr")
|
|
63
70
|
yield { stream: event.type, data: event.data };
|
|
64
71
|
if (event.type === "exit")
|
|
@@ -68,6 +75,8 @@ export class Command {
|
|
|
68
75
|
catch (error) {
|
|
69
76
|
if (abort.signal.aborted)
|
|
70
77
|
return;
|
|
78
|
+
if (error instanceof StreamError)
|
|
79
|
+
throw error;
|
|
71
80
|
const translated = translate(error, sandboxName);
|
|
72
81
|
throw translated instanceof Error && translated !== error
|
|
73
82
|
? translated
|
|
@@ -94,6 +103,8 @@ export class Command {
|
|
|
94
103
|
let exit;
|
|
95
104
|
await guard(async () => {
|
|
96
105
|
for await (const event of this.init.process.output(params.signal ? { signal: params.signal } : {})) {
|
|
106
|
+
if (event.type === "truncated")
|
|
107
|
+
throw new StreamError("output_truncated", "Some command output is no longer available.", this.init.process.id);
|
|
97
108
|
if (event.type === "stdout" || event.type === "stderr")
|
|
98
109
|
lines.push({ stream: event.type, data: event.data });
|
|
99
110
|
else if (event.type === "exit")
|
package/dist/vercel/errors.d.ts
CHANGED
|
@@ -44,8 +44,8 @@ export declare function translate(error: unknown, sandboxName?: string): unknown
|
|
|
44
44
|
/** Runs `work`, turning any Runtime error into Vercel's. */
|
|
45
45
|
export declare function guard<T>(work: () => Promise<T>, sandboxName?: string): Promise<T>;
|
|
46
46
|
/** A node:fs-style error, as Vercel's `sandbox.fs` throws. */
|
|
47
|
-
export declare function fsError(code: "ENOENT" | "EEXIST" | "EISDIR" | "ENOTDIR", syscall: string, path: string): Error & {
|
|
48
|
-
code: "ENOENT" | "EEXIST" | "EISDIR" | "
|
|
47
|
+
export declare function fsError(code: "ENOENT" | "EEXIST" | "EISDIR" | "ENOTDIR" | "EACCES" | "ENOTEMPTY" | "EINVAL", syscall: string, path: string): Error & {
|
|
48
|
+
code: "ENOENT" | "EEXIST" | "ENOTDIR" | "EISDIR" | "EACCES" | "ENOTEMPTY" | "EINVAL";
|
|
49
49
|
errno: number;
|
|
50
50
|
syscall: string;
|
|
51
51
|
path: string;
|
package/dist/vercel/errors.js
CHANGED
|
@@ -102,8 +102,19 @@ export function fsError(code, syscall, path) {
|
|
|
102
102
|
EEXIST: "file already exists",
|
|
103
103
|
EISDIR: "illegal operation on a directory",
|
|
104
104
|
ENOTDIR: "not a directory",
|
|
105
|
+
EACCES: "permission denied",
|
|
106
|
+
ENOTEMPTY: "directory not empty",
|
|
107
|
+
EINVAL: "invalid argument",
|
|
108
|
+
}[code];
|
|
109
|
+
const errno = {
|
|
110
|
+
ENOENT: -2,
|
|
111
|
+
EEXIST: -17,
|
|
112
|
+
EISDIR: -21,
|
|
113
|
+
ENOTDIR: -20,
|
|
114
|
+
EACCES: -13,
|
|
115
|
+
ENOTEMPTY: -39,
|
|
116
|
+
EINVAL: -22,
|
|
105
117
|
}[code];
|
|
106
|
-
const errno = { ENOENT: -2, EEXIST: -17, EISDIR: -21, ENOTDIR: -20 }[code];
|
|
107
118
|
return Object.assign(new Error(`${code}: ${text}, ${syscall} '${path}'`), {
|
|
108
119
|
code,
|
|
109
120
|
errno,
|