withruntime 0.8.4 → 0.10.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/COMPATIBILITY.md +145 -0
- package/DAYTONA.md +13 -7
- package/E2B.md +34 -20
- package/README.md +22 -11
- package/README.src.md +22 -11
- package/VERCEL.md +13 -3
- package/compatibility-lock.json +357 -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/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 +3 -2
- package/dist/blaxel/preview.js +16 -14
- package/dist/blaxel/process.d.ts +1 -1
- package/dist/blaxel/process.js +66 -11
- 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-words.d.ts +27 -0
- package/dist/cli-words.js +135 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +351 -85
- package/dist/client.d.ts +33 -4
- package/dist/client.js +47 -12
- package/dist/cloudflare/identity.d.ts +7 -0
- package/dist/cloudflare/identity.js +13 -0
- package/dist/cloudflare/index.d.ts +303 -0
- package/dist/cloudflare/index.js +592 -0
- package/dist/cloudflare/worker.d.ts +419 -0
- package/dist/cloudflare/worker.js +447 -0
- package/dist/codesandbox/guest-service.d.ts +31 -0
- package/dist/codesandbox/guest-service.js +490 -0
- package/dist/codesandbox/index.d.ts +386 -0
- package/dist/codesandbox/index.js +1347 -0
- package/dist/compat/core.d.ts +40 -0
- package/dist/compat/core.js +238 -0
- package/dist/compat/files.d.ts +26 -0
- package/dist/compat/files.js +133 -0
- package/dist/compat/shell.d.ts +4 -0
- package/dist/compat/shell.js +165 -0
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +107 -38
- package/dist/daytona/context.js +2 -1
- package/dist/daytona/daytona.js +11 -4
- package/dist/daytona/filesystem.d.ts +17 -4
- package/dist/daytona/filesystem.js +121 -38
- package/dist/daytona/index.d.ts +1 -1
- package/dist/daytona/process.d.ts +1 -1
- package/dist/daytona/process.js +27 -4
- package/dist/daytona/sandbox.d.ts +1 -1
- package/dist/daytona/sandbox.js +16 -7
- package/dist/e2b/client.js +1 -1
- package/dist/e2b/code-interpreter.d.ts +4 -5
- package/dist/e2b/code-interpreter.js +54 -56
- package/dist/e2b/commands.d.ts +35 -12
- package/dist/e2b/commands.js +169 -73
- 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/index.d.ts +1 -0
- package/dist/e2b/index.js +1 -0
- package/dist/e2b/pty.d.ts +24 -0
- package/dist/e2b/pty.js +80 -0
- package/dist/e2b/sandbox.d.ts +6 -4
- package/dist/e2b/sandbox.js +59 -25
- package/dist/errors.d.ts +8 -0
- package/dist/errors.js +15 -2
- package/dist/file-source.d.ts +16 -0
- package/dist/file-source.js +162 -0
- package/dist/freestyle/index.d.ts +309 -0
- package/dist/freestyle/index.js +343 -0
- package/dist/index.d.ts +3 -1
- package/dist/login.js +16 -10
- package/dist/mcp.js +2 -2
- package/dist/modal/index.d.ts +205 -0
- package/dist/modal/index.js +486 -0
- package/dist/network-cli.js +112 -44
- package/dist/openai-agents/index.js +18 -71
- package/dist/page.d.ts +1 -1
- package/dist/page.js +2 -2
- package/dist/products/audit.d.ts +3 -0
- package/dist/products/index.d.ts +14 -0
- package/dist/products/index.js +12 -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/observability.d.ts +44 -0
- package/dist/products/observability.js +6 -0
- package/dist/products/previews.d.ts +18 -4
- package/dist/products/previews.js +14 -5
- package/dist/products/tailscale.d.ts +57 -0
- package/dist/products/tailscale.js +17 -0
- package/dist/products/volumes.d.ts +42 -0
- package/dist/products/volumes.js +36 -0
- package/dist/products/watch.d.ts +25 -4
- package/dist/products/watch.js +132 -18
- package/dist/proxy.js +3 -1
- package/dist/runloop/index.d.ts +326 -0
- package/dist/runloop/index.js +535 -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 +76 -16
- package/dist/sandbox.js +696 -135
- package/dist/snapshots.d.ts +20 -5
- package/dist/snapshots.js +11 -1
- package/dist/sprites/index.d.ts +251 -0
- package/dist/sprites/index.js +588 -0
- package/dist/tar.d.ts +10 -1
- package/dist/tar.js +311 -35
- package/dist/tools/index.js +2 -1
- package/dist/transport.d.ts +23 -8
- package/dist/transport.js +247 -127
- package/dist/types.d.ts +52 -3
- package/dist/vercel/command.d.ts +1 -1
- package/dist/vercel/command.js +15 -3
- package/dist/vercel/errors.d.ts +2 -2
- package/dist/vercel/errors.js +19 -2
- package/dist/vercel/filesystem.d.ts +21 -18
- package/dist/vercel/filesystem.js +95 -48
- package/dist/vercel/sandbox.d.ts +8 -8
- package/dist/vercel/sandbox.js +159 -64
- package/dist/vercel/snapshot.d.ts +3 -2
- package/dist/vercel/snapshot.js +9 -5
- package/package.json +46 -3
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.10.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() {
|
|
@@ -49,18 +80,57 @@ const sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
|
49
80
|
else
|
|
50
81
|
signal?.addEventListener("abort", abort, { once: true });
|
|
51
82
|
});
|
|
83
|
+
/** A credential lookup can finish independently, but must not keep its
|
|
84
|
+
* caller waiting after cancellation or hold that caller's queue slot. */
|
|
85
|
+
function untilAborted(pending, signal) {
|
|
86
|
+
if (!signal)
|
|
87
|
+
return pending;
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
const abort = () => reject(abortReason(signal));
|
|
90
|
+
if (signal.aborted)
|
|
91
|
+
abort();
|
|
92
|
+
else
|
|
93
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
94
|
+
pending.then((value) => {
|
|
95
|
+
signal.removeEventListener("abort", abort);
|
|
96
|
+
resolve(value);
|
|
97
|
+
}, (cause) => {
|
|
98
|
+
signal.removeEventListener("abort", abort);
|
|
99
|
+
reject(cause instanceof Error ? cause : new Error("Credential lookup failed.", { cause }));
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function abortReason(signal) {
|
|
104
|
+
return signal?.reason instanceof Error
|
|
105
|
+
? signal.reason
|
|
106
|
+
: new Error("Aborted", { cause: signal?.reason });
|
|
107
|
+
}
|
|
52
108
|
/** At most `maximum` holders at once; the rest wait their turn, first come
|
|
53
109
|
* first served. */
|
|
54
110
|
function slots(maximum) {
|
|
55
111
|
let active = 0;
|
|
56
112
|
const waiting = [];
|
|
57
113
|
return {
|
|
58
|
-
async take() {
|
|
114
|
+
async take(signal) {
|
|
115
|
+
signal?.throwIfAborted();
|
|
59
116
|
if (active < maximum) {
|
|
60
117
|
active++;
|
|
61
118
|
return;
|
|
62
119
|
}
|
|
63
|
-
await new Promise((resolve) =>
|
|
120
|
+
await new Promise((resolve, reject) => {
|
|
121
|
+
const ready = () => {
|
|
122
|
+
signal?.removeEventListener("abort", abort);
|
|
123
|
+
resolve();
|
|
124
|
+
};
|
|
125
|
+
const abort = () => {
|
|
126
|
+
const at = waiting.indexOf(ready);
|
|
127
|
+
if (at >= 0)
|
|
128
|
+
waiting.splice(at, 1);
|
|
129
|
+
reject(abortReason(signal));
|
|
130
|
+
};
|
|
131
|
+
waiting.push(ready);
|
|
132
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
133
|
+
});
|
|
64
134
|
},
|
|
65
135
|
give() {
|
|
66
136
|
const next = waiting.shift();
|
|
@@ -99,7 +169,7 @@ export class Transport {
|
|
|
99
169
|
throw missingKey();
|
|
100
170
|
else
|
|
101
171
|
this.#apiKey = options.apiKey;
|
|
102
|
-
this.baseUrl = apiOrigin(options.baseUrl
|
|
172
|
+
this.baseUrl = apiOrigin(options.baseUrl ? reachable(options.baseUrl) : defaultBaseUrl());
|
|
103
173
|
// The environment's proxy (HTTPS_PROXY, NO_PROXY) on Node and Bun alike;
|
|
104
174
|
// a fetch passed in is used as it is.
|
|
105
175
|
this.#fetch = options.fetch ?? envFetch;
|
|
@@ -139,21 +209,71 @@ export class Transport {
|
|
|
139
209
|
* the same key every time, and waits out a full house for a call that sets
|
|
140
210
|
* waitForCapacityMs. */
|
|
141
211
|
async send(call) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
212
|
+
return this.#send(this.#prepare(call), async (response) => response);
|
|
213
|
+
}
|
|
214
|
+
/** Start the deadline before waiting for a connection; keep one key and
|
|
215
|
+
* signal through every retry and the complete response body. */
|
|
216
|
+
#prepare(call) {
|
|
217
|
+
const timeoutMs = call.timeoutMs ?? this.#timeoutMs;
|
|
218
|
+
const roomMs = Math.max(0, call.waitForCapacityMs ?? 0);
|
|
219
|
+
const deadline = timeoutMs === 0 ? undefined : AbortSignal.timeout(timeoutMs + roomMs);
|
|
220
|
+
const signal = deadline
|
|
221
|
+
? call.signal
|
|
222
|
+
? AbortSignal.any([call.signal, deadline])
|
|
223
|
+
: deadline
|
|
224
|
+
: call.signal;
|
|
225
|
+
return {
|
|
226
|
+
...call,
|
|
227
|
+
timeoutMs: 0,
|
|
228
|
+
...(signal ? { signal } : {}),
|
|
229
|
+
...(call.method !== "GET"
|
|
230
|
+
? { idempotencyKey: call.idempotencyKey ?? crypto.randomUUID() }
|
|
231
|
+
: {}),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
#connectionError(call, cause) {
|
|
235
|
+
const cancelled = call.signal?.aborted === true;
|
|
236
|
+
const route = this.#routed && !cancelled
|
|
237
|
+
? describeRoute(`${this.baseUrl}${call.path}`, cause)
|
|
238
|
+
: { via: "" };
|
|
239
|
+
return new ConnectionError({
|
|
240
|
+
message: cancelled
|
|
241
|
+
? "The call was cancelled or ran past its deadline."
|
|
242
|
+
: call.method !== "GET"
|
|
243
|
+
? `No answer from Runtime at ${this.baseUrl}${route.via}. The change may have happened; retrying with the same idempotencyKey is safe.`
|
|
244
|
+
: `No answer from Runtime at ${this.baseUrl}${route.via}.`,
|
|
245
|
+
hint: cancelled
|
|
246
|
+
? "Raise timeoutMs for a long call, or retry with the same idempotencyKey."
|
|
247
|
+
: (route.hint ?? "Check the network, and RUNTIME_API_URL if you set it."),
|
|
248
|
+
code: cancelled ? "timeout" : "connection_error",
|
|
249
|
+
status: 0,
|
|
250
|
+
...(call.idempotencyKey ? { idempotencyKey: call.idempotencyKey } : {}),
|
|
251
|
+
cause,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
async #send(call, consume) {
|
|
255
|
+
let apiKey;
|
|
256
|
+
try {
|
|
257
|
+
call.signal?.throwIfAborted();
|
|
258
|
+
apiKey = await untilAborted(this.#key(), call.signal);
|
|
259
|
+
}
|
|
260
|
+
catch (cause) {
|
|
261
|
+
if (call.signal?.aborted)
|
|
262
|
+
throw this.#connectionError(call, cause);
|
|
263
|
+
throw cause;
|
|
264
|
+
}
|
|
265
|
+
const key = call.method !== "GET" ? call.idempotencyKey : undefined;
|
|
147
266
|
const roomMs = Math.max(0, call.waitForCapacityMs ?? 0);
|
|
148
267
|
const roomUntil = performance.now() + roomMs;
|
|
149
268
|
let roomAttempt = 0;
|
|
150
|
-
const
|
|
151
|
-
const signal = call.signal ? AbortSignal.any([call.signal, deadline]) : deadline;
|
|
269
|
+
const signal = call.signal;
|
|
152
270
|
const url = `${this.baseUrl}${call.path}${encodeQuery(call.query)}`;
|
|
153
271
|
const body = call.bytes ?? (call.body === undefined ? undefined : JSON.stringify(call.body));
|
|
154
272
|
const retry = call.retry ?? true;
|
|
155
|
-
|
|
273
|
+
let attempt = 0;
|
|
274
|
+
for (;;) {
|
|
156
275
|
let response;
|
|
276
|
+
let inCallback = false;
|
|
157
277
|
// Called as a plain function: Cloudflare Workers' fetch throws "Illegal
|
|
158
278
|
// invocation" when called as a method of another object (this.#fetch()).
|
|
159
279
|
const send = this.#fetch;
|
|
@@ -161,6 +281,7 @@ export class Transport {
|
|
|
161
281
|
// "manual", not "error": Workers accept only "follow" and "manual".
|
|
162
282
|
// The API never redirects, so a redirect is refused here exactly as
|
|
163
283
|
// "error" refused it: thrown, retried, then a connection_error.
|
|
284
|
+
signal?.throwIfAborted();
|
|
164
285
|
response = await send(url, {
|
|
165
286
|
method: call.method,
|
|
166
287
|
headers: this.#headers(call, key, apiKey),
|
|
@@ -173,136 +294,132 @@ export class Transport {
|
|
|
173
294
|
await response.body?.cancel().catch(() => { });
|
|
174
295
|
throw new TypeError(`fetch failed: Runtime answered a redirect (${response.status}).`);
|
|
175
296
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
297
|
+
/* An answer that took longer than the API holds its headers (90 s)
|
|
298
|
+
comes as a 200 sent early, and then its JSON: the answer, or
|
|
299
|
+
{"error": ...} when the work failed after the status went
|
|
300
|
+
(runtime-late-answer, api.md). Either way it is read here, so a
|
|
301
|
+
failure is thrown as the error it is, never returned as a success. */
|
|
302
|
+
let late;
|
|
303
|
+
if (response.ok && response.headers.get("runtime-late-answer") === "true") {
|
|
304
|
+
const text = await response.text();
|
|
305
|
+
const failed = lateFailure(text);
|
|
306
|
+
if (failed === undefined)
|
|
307
|
+
return await consume(new Response(text, { headers: response.headers }));
|
|
308
|
+
late = { status: failed, text };
|
|
309
|
+
}
|
|
310
|
+
else if (response.ok) {
|
|
311
|
+
inCallback = true;
|
|
312
|
+
call.onResponse?.(response);
|
|
313
|
+
inCallback = false;
|
|
314
|
+
return await consume(response);
|
|
315
|
+
}
|
|
316
|
+
const status = late?.status ?? response.status;
|
|
317
|
+
// Runtime never answers 407: it is a proxy refusing the tunnel, which
|
|
318
|
+
// Bun's fetch hands back as a response rather than an error.
|
|
319
|
+
if (response.status === 407 && this.#routed) {
|
|
320
|
+
await response.body?.cancel();
|
|
321
|
+
const route = describeRoute(url, 407);
|
|
183
322
|
throw new ConnectionError({
|
|
184
|
-
message:
|
|
185
|
-
|
|
186
|
-
: write
|
|
187
|
-
? `No answer from Runtime at ${this.baseUrl}${route.via}. The change may have happened; retrying with the same idempotencyKey is safe.`
|
|
188
|
-
: `No answer from Runtime at ${this.baseUrl}${route.via}.`,
|
|
189
|
-
hint: signal.aborted
|
|
190
|
-
? "Raise timeoutMs for a long call, or retry with the same idempotencyKey."
|
|
191
|
-
: (route.hint ?? "Check the network, and RUNTIME_API_URL if you set it."),
|
|
192
|
-
code: signal.aborted ? "timeout" : "connection_error",
|
|
323
|
+
message: `No answer from Runtime at ${this.baseUrl}${route.via}.`,
|
|
324
|
+
code: "connection_error",
|
|
193
325
|
status: 0,
|
|
326
|
+
...(route.hint ? { hint: route.hint } : {}),
|
|
194
327
|
...(key ? { idempotencyKey: key } : {}),
|
|
195
|
-
cause,
|
|
196
328
|
});
|
|
197
329
|
}
|
|
198
|
-
await
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
|
|
330
|
+
const text = late?.text ?? (await response.text());
|
|
331
|
+
let parsed = null;
|
|
332
|
+
try {
|
|
333
|
+
parsed = JSON.parse(text);
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
parsed = { error: { message: text.slice(0, 500) || `HTTP ${status}` } };
|
|
337
|
+
}
|
|
338
|
+
const error = errorFor(status, parsed, key);
|
|
339
|
+
// A full house: wait for a slot or for room, then send the same call
|
|
340
|
+
// again. A request that can never fit (a fork of more copies than the
|
|
341
|
+
// trial runs) is not waited for.
|
|
342
|
+
if (retry &&
|
|
343
|
+
roomMs > 0 &&
|
|
344
|
+
WAITS_FOR_ROOM.has(error.code) &&
|
|
345
|
+
error.details?.field !== "count") {
|
|
346
|
+
const left = roomUntil - performance.now();
|
|
347
|
+
if (left <= 0)
|
|
348
|
+
throw error;
|
|
349
|
+
const pause = Math.min(left, error.retryAfterMs ?? roomBackoff(roomAttempt++));
|
|
350
|
+
inCallback = true;
|
|
351
|
+
call.onCapacityWait?.(error, pause);
|
|
352
|
+
inCallback = false;
|
|
353
|
+
await sleep(pause, signal);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
const retryable = [429, 502, 503, 504].includes(status) && !DELIBERATE.has(error.code);
|
|
357
|
+
if (!retry || !retryable || attempt >= this.#maxRetries)
|
|
358
|
+
throw error;
|
|
359
|
+
const header = Number(response.headers.get("retry-after"));
|
|
360
|
+
const wait = error.retryAfterMs ??
|
|
361
|
+
(Number.isFinite(header) && header > 0 ? header * 1000 : backoff(attempt));
|
|
362
|
+
await sleep(Math.min(30_000, wait) * (0.9 + Math.random() * 0.2), signal);
|
|
213
363
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
364
|
+
catch (cause) {
|
|
365
|
+
// Deliberate API refusals and unusable proxy settings cannot be
|
|
366
|
+
// fixed by repeating the request. Body failures are transport
|
|
367
|
+
// failures too: a write replays under its original key.
|
|
368
|
+
if (inCallback) {
|
|
369
|
+
await response?.body?.cancel().catch(() => undefined);
|
|
370
|
+
throw cause;
|
|
371
|
+
}
|
|
372
|
+
if (cause instanceof RuntimeError)
|
|
373
|
+
throw cause;
|
|
374
|
+
if (signal?.aborted || !retry || attempt >= this.#maxRetries)
|
|
375
|
+
throw this.#connectionError(call, cause);
|
|
376
|
+
await sleep(backoff(attempt), signal).catch((cause) => {
|
|
377
|
+
throw this.#connectionError(call, cause);
|
|
228
378
|
});
|
|
229
379
|
}
|
|
230
|
-
|
|
231
|
-
let parsed = null;
|
|
232
|
-
try {
|
|
233
|
-
parsed = JSON.parse(text);
|
|
234
|
-
}
|
|
235
|
-
catch {
|
|
236
|
-
parsed = { error: { message: text.slice(0, 500) || `HTTP ${status}` } };
|
|
237
|
-
}
|
|
238
|
-
const error = errorFor(status, parsed, key);
|
|
239
|
-
// A full house: wait for a slot or for room, then send the same call
|
|
240
|
-
// again. A request that can never fit (a fork of more copies than the
|
|
241
|
-
// trial runs) is not waited for.
|
|
242
|
-
if (retry &&
|
|
243
|
-
roomMs > 0 &&
|
|
244
|
-
WAITS_FOR_ROOM.has(error.code) &&
|
|
245
|
-
error.details?.field !== "count") {
|
|
246
|
-
const left = roomUntil - performance.now();
|
|
247
|
-
if (left <= 0)
|
|
248
|
-
throw error;
|
|
249
|
-
const pause = Math.min(left, error.retryAfterMs ?? roomBackoff(roomAttempt++));
|
|
250
|
-
call.onCapacityWait?.(error, pause);
|
|
251
|
-
await sleep(pause, signal);
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
const retryable = [429, 502, 503, 504].includes(status) && !DELIBERATE.has(error.code);
|
|
255
|
-
if (!retry || !retryable || attempt >= this.#maxRetries)
|
|
256
|
-
throw error;
|
|
257
|
-
const header = Number(response.headers.get("retry-after"));
|
|
258
|
-
const wait = error.retryAfterMs ??
|
|
259
|
-
(Number.isFinite(header) && header > 0 ? header * 1000 : backoff(attempt));
|
|
260
|
-
await sleep(Math.min(30_000, wait) * (0.9 + Math.random() * 0.2), signal);
|
|
380
|
+
attempt++;
|
|
261
381
|
}
|
|
262
382
|
}
|
|
263
|
-
async
|
|
264
|
-
await this.#slots.take();
|
|
383
|
+
async #queued(call, consume) {
|
|
265
384
|
try {
|
|
266
|
-
|
|
267
|
-
return (text ? JSON.parse(text) : null);
|
|
385
|
+
await this.#slots.take(call.signal);
|
|
268
386
|
}
|
|
269
|
-
|
|
270
|
-
this.#
|
|
387
|
+
catch (cause) {
|
|
388
|
+
throw this.#connectionError(call, cause);
|
|
271
389
|
}
|
|
272
|
-
}
|
|
273
|
-
async bytes(call) {
|
|
274
|
-
await this.#slots.take();
|
|
275
390
|
try {
|
|
276
|
-
return
|
|
391
|
+
return await this.#send(call, consume);
|
|
277
392
|
}
|
|
278
393
|
finally {
|
|
279
394
|
this.#slots.give();
|
|
280
395
|
}
|
|
281
396
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
397
|
+
async json(call) {
|
|
398
|
+
return this.#queued(this.#prepare(call), async (response) => {
|
|
399
|
+
const text = await response.text();
|
|
400
|
+
return (text ? JSON.parse(text) : null);
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
async bytes(call) {
|
|
404
|
+
return this.#queued(this.#prepare(call), async (response) => new Uint8Array(await response.arrayBuffer()));
|
|
405
|
+
}
|
|
406
|
+
/** A file's bytes, read whole and checked against its promised length and
|
|
407
|
+
* SHA-256. A short or different body is read again twice, within the same
|
|
408
|
+
* call deadline. */
|
|
288
409
|
async fileBytes(call) {
|
|
410
|
+
call = this.#prepare(call);
|
|
289
411
|
for (let attempt = 0;; attempt++) {
|
|
290
|
-
await this.#
|
|
291
|
-
let failure;
|
|
292
|
-
try {
|
|
293
|
-
const response = await this.send(call);
|
|
412
|
+
const { body, problem } = await this.#queued(call, async (response) => {
|
|
294
413
|
const body = new Uint8Array(await response.arrayBuffer());
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
throw failure;
|
|
305
|
-
await sleep(backoff(attempt), call.signal);
|
|
414
|
+
return { body, problem: await checkBody(response.headers, body) };
|
|
415
|
+
});
|
|
416
|
+
if (!problem)
|
|
417
|
+
return body;
|
|
418
|
+
if (attempt >= 2)
|
|
419
|
+
throw problem;
|
|
420
|
+
await sleep(backoff(attempt), call.signal).catch((cause) => {
|
|
421
|
+
throw this.#connectionError(call, cause);
|
|
422
|
+
});
|
|
306
423
|
}
|
|
307
424
|
}
|
|
308
425
|
/** A file's bytes as they arrive, for files too big to hold. The stream
|
|
@@ -336,12 +453,14 @@ export class Transport {
|
|
|
336
453
|
let buffer = "";
|
|
337
454
|
try {
|
|
338
455
|
for (;;) {
|
|
456
|
+
call.signal?.throwIfAborted();
|
|
339
457
|
const { value, done } = await reader.read();
|
|
340
458
|
if (done)
|
|
341
459
|
break;
|
|
342
460
|
buffer += value;
|
|
343
461
|
let newline = buffer.indexOf("\n");
|
|
344
462
|
while (newline >= 0) {
|
|
463
|
+
call.signal?.throwIfAborted();
|
|
345
464
|
const line = buffer.slice(0, newline).trim();
|
|
346
465
|
buffer = buffer.slice(newline + 1);
|
|
347
466
|
if (line)
|
|
@@ -349,6 +468,7 @@ export class Transport {
|
|
|
349
468
|
newline = buffer.indexOf("\n");
|
|
350
469
|
}
|
|
351
470
|
}
|
|
471
|
+
call.signal?.throwIfAborted();
|
|
352
472
|
if (buffer.trim())
|
|
353
473
|
yield JSON.parse(buffer);
|
|
354
474
|
}
|
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.d.ts
CHANGED