withruntime 0.10.0 → 0.11.1
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 +7 -8
- package/DAYTONA.md +40 -35
- package/E2B.md +70 -57
- package/README.md +3 -3
- package/README.src.md +3 -3
- package/VERCEL.md +23 -20
- package/compatibility-lock.json +63 -84
- package/dist/api-defaults.d.ts +1 -1
- package/dist/api-defaults.js +1 -1
- package/dist/blaxel/lookup.d.ts +2 -1
- package/dist/blaxel/lookup.js +2 -1
- package/dist/blaxel/sandbox.d.ts +7 -5
- package/dist/blaxel/sandbox.js +15 -10
- package/dist/cli.js +112 -17
- package/dist/client.d.ts +17 -1
- package/dist/client.js +46 -3
- package/dist/compat/files.js +5 -1
- package/dist/compat/lease.d.ts +37 -0
- package/dist/compat/lease.js +93 -0
- package/dist/credentials.d.ts +9 -0
- package/dist/credentials.js +26 -4
- 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 +150 -26
- package/dist/daytona/filesystem.js +8 -0
- package/dist/daytona/index.d.ts +2 -26
- 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 +25 -6
- package/dist/daytona/sandbox.js +60 -27
- package/dist/e2b/client.d.ts +5 -0
- package/dist/e2b/client.js +5 -1
- package/dist/e2b/commands.d.ts +15 -6
- package/dist/e2b/commands.js +83 -18
- 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 +2 -2
- package/dist/e2b/index.js +1 -1
- package/dist/e2b/pty.js +6 -3
- package/dist/e2b/sandbox.d.ts +20 -6
- package/dist/e2b/sandbox.js +207 -31
- package/dist/e2b/users.d.ts +30 -0
- package/dist/e2b/users.js +185 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +6 -0
- package/dist/identity.js +2 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/dist/mcp.d.ts +2 -1
- package/dist/mcp.js +6 -1
- package/dist/modal/index.js +6 -2
- package/dist/network-cli.js +33 -5
- package/dist/products/browser.d.ts +61 -0
- package/dist/products/browser.js +27 -0
- package/dist/products/desktop.d.ts +4 -3
- package/dist/products/desktop.js +7 -32
- package/dist/products/images.d.ts +3 -1
- package/dist/products/images.js +14 -2
- package/dist/products/index.d.ts +2 -0
- package/dist/products/index.js +2 -0
- package/dist/products/jobs.d.ts +7 -5
- package/dist/products/jobs.js +7 -3
- package/dist/products/mcp.d.ts +5 -1
- package/dist/products/mcp.js +29 -8
- package/dist/products/network-products.d.ts +8 -1
- package/dist/products/network-products.js +4 -1
- package/dist/products/observability.d.ts +1 -1
- package/dist/products/private-network.d.ts +22 -0
- package/dist/products/private-network.js +10 -0
- package/dist/products/secrets.d.ts +2 -2
- package/dist/runloop/index.js +6 -2
- package/dist/sandbox.d.ts +44 -14
- package/dist/sandbox.js +162 -48
- package/dist/tar.d.ts +2 -0
- package/dist/tar.js +74 -16
- package/dist/transport.d.ts +33 -7
- package/dist/transport.js +171 -36
- package/dist/types.d.ts +59 -11
- package/dist/vercel/filesystem.js +3 -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 +57 -15
- package/dist/vercel/sandbox.js +219 -47
- package/dist/wait.d.ts +12 -0
- package/dist/wait.js +47 -0
- package/package.json +1 -1
|
@@ -4,7 +4,7 @@ import type { RequestOptions, Transport } from "../transport.js";
|
|
|
4
4
|
* absent) and `paths`, each exact (`/v1/chat/completions`) or a prefix ending
|
|
5
5
|
* in `/*` (`/repos/acme/*`), written canonically: starting with `/`, no `.` or
|
|
6
6
|
* `..` segments, no `;` or `\`, no encoded slash. A request whose path could be
|
|
7
|
-
* read two ways gets no secret that has rules.
|
|
7
|
+
* read two ways gets no secret that has rules. Needs credit on the account. */
|
|
8
8
|
export type SecretRule = {
|
|
9
9
|
methods?: string[];
|
|
10
10
|
paths: string[];
|
|
@@ -37,7 +37,7 @@ export type SetSecret = {
|
|
|
37
37
|
header?: string;
|
|
38
38
|
/** With `header`: its value, `{value}` where the secret goes. Default `{value}`. */
|
|
39
39
|
format?: string;
|
|
40
|
-
/** Only requests a rule allows carry the value.
|
|
40
|
+
/** Only requests a rule allows carry the value. Needs credit on the account. 1 to 16.
|
|
41
41
|
* Absent: every request to the hosts; replacing a secret without rules
|
|
42
42
|
* clears them. */
|
|
43
43
|
rules?: SecretRule[];
|
package/dist/runloop/index.js
CHANGED
|
@@ -172,8 +172,12 @@ export class Devboxes {
|
|
|
172
172
|
vcpu: p.custom_cpu_cores ?? size?.[0],
|
|
173
173
|
memoryMiB: p.custom_gb_memory ? p.custom_gb_memory * 1024 : size?.[1],
|
|
174
174
|
diskMiB: p.custom_disk_size ? p.custom_disk_size * 1024 : size?.[2],
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
// keep_alive_time_seconds is the customer's limit, with no idle pause
|
|
176
|
+
// before it. None is no time limit (0300): it runs while it works and
|
|
177
|
+
// pauses when idle.
|
|
178
|
+
...(p.keep_alive_time_seconds === undefined || p.keep_alive_time_seconds === null
|
|
179
|
+
? {}
|
|
180
|
+
: { timeoutSeconds: p.keep_alive_time_seconds, idlePauseSeconds: 0 }),
|
|
177
181
|
onLeaseEnd: "stop",
|
|
178
182
|
}, input.environment_variables ?? undefined, async (sandbox) => {
|
|
179
183
|
for (const [path, content] of Object.entries(input.file_mounts ?? {})) {
|
package/dist/sandbox.d.ts
CHANGED
|
@@ -105,12 +105,15 @@ export declare class Sandbox implements AsyncDisposable {
|
|
|
105
105
|
* lists. Its snapshots, usage and audit entries stay. Deleting it again
|
|
106
106
|
* answers the same. */
|
|
107
107
|
delete(options?: RequestOptions): Promise<DeletedSandbox>;
|
|
108
|
-
/** Carries on a paused sandbox;
|
|
108
|
+
/** Carries on a paused sandbox; for one with a time limit,
|
|
109
|
+
* `timeoutSeconds` is its new one. */
|
|
109
110
|
wake(options?: RequestOptions & {
|
|
110
111
|
wait?: boolean;
|
|
111
112
|
timeoutSeconds?: number;
|
|
112
113
|
}): Promise<this>;
|
|
113
|
-
/** More time before
|
|
114
|
+
/** More time before its time limit ends (at most an hour ahead of now). A
|
|
115
|
+
* sandbox with no time limit needs none: the call answers at once and
|
|
116
|
+
* changes nothing. */
|
|
114
117
|
extend(seconds: number, options?: RequestOptions): Promise<this>;
|
|
115
118
|
/** Changes its name, labels, automatic wake, idle pause or persistence;
|
|
116
119
|
* fields left out stay as they are. `persistent: true` keeps it running
|
|
@@ -120,18 +123,36 @@ export declare class Sandbox implements AsyncDisposable {
|
|
|
120
123
|
* its id, /workspace (its home: dotfiles, pip --user and npm -g installs),
|
|
121
124
|
* volumes, environment, name and previews. Its processes restart, and
|
|
122
125
|
* everything else on its old disk (sudo installs, apt packages, /etc) is
|
|
123
|
-
* lost
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
|
|
128
|
-
|
|
126
|
+
* lost; snapshot it first to keep everything. A running sandbox is paused
|
|
127
|
+
* first. A switch that fails is undone (`switch_undone`), the sandbox on
|
|
128
|
+
* its old image with nothing lost. Charged as a wake. `keep` is optional:
|
|
129
|
+
* "workspace" is the only value, and it is sent either way, so an API from
|
|
130
|
+
* before it was optional accepts the call too. */
|
|
131
|
+
switchImage(image: string, options?: RequestOptions & {
|
|
132
|
+
keep?: "workspace";
|
|
129
133
|
}): Promise<this>;
|
|
130
|
-
/**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
134
|
+
/** Gives it more or fewer vCPUs or more or less memory by a restart: its id,
|
|
135
|
+
* whole disk, volumes, environment, name and previews stay, and its
|
|
136
|
+
* programs stop (snapshot it first to keep its memory too). A running sandbox is paused, or stopped if it is
|
|
137
|
+
* persistent, and comes back running at the new size on the same server; a
|
|
138
|
+
* paused or stopped one is started. Memory bills on the new size from then.
|
|
139
|
+
* Refused, with nothing changed, when the server has no room
|
|
140
|
+
* (`no_capacity`), above your quota or the trial's 2 vCPU and 4 GiB, or
|
|
141
|
+
* while a snapshot or fork of it is being taken. `restart` is optional:
|
|
142
|
+
* true is the only value, and it is sent either way, so an API from before
|
|
143
|
+
* it was optional accepts the call too. */
|
|
144
|
+
resize(size: {
|
|
145
|
+
vcpu?: number;
|
|
146
|
+
memoryMiB?: number;
|
|
147
|
+
}, options?: RequestOptions & {
|
|
148
|
+
restart?: true;
|
|
149
|
+
}): Promise<this>;
|
|
150
|
+
/** Keeps a sandbox with a time limit running past it, in the background,
|
|
151
|
+
* until stop() or the returned function ends it: every `everySeconds` (60)
|
|
152
|
+
* it extends the limit so that `marginSeconds` (600) remain, never more
|
|
153
|
+
* than the hour ahead the API allows. A sandbox with no time limit
|
|
154
|
+
* (`endsAt` null) needs none, and is only watched. Running time is billed
|
|
155
|
+
* as it is used. A paused sandbox is left paused (a request wakes it,
|
|
135
156
|
* unless autoWake is off); a stopped one ends the loop. It does not keep a
|
|
136
157
|
* Node or Bun process alive by itself. */
|
|
137
158
|
keepAlive(options?: KeepAliveOptions): () => void;
|
|
@@ -139,7 +160,9 @@ export declare class Sandbox implements AsyncDisposable {
|
|
|
139
160
|
setRetention(days: number, options?: RequestOptions): Promise<this>;
|
|
140
161
|
/** Keeps this sandbox's whole machine (files, memory, running processes) as
|
|
141
162
|
* a snapshot to start new sandboxes from. A running sandbox is paused for
|
|
142
|
-
* the moment it takes, then woken; a paused one stays paused.
|
|
163
|
+
* the moment it takes, then woken; a paused one stays paused. Past
|
|
164
|
+
* `timeoutMs` (ten minutes unless given) mid-capture, it stays paused and
|
|
165
|
+
* the error names the snapshot. */
|
|
143
166
|
snapshot(options?: SnapshotOptions & RequestOptions): Promise<Snapshot>;
|
|
144
167
|
/** Copies of this sandbox as it is now (files, memory, running processes),
|
|
145
168
|
* each its own sandbox, answered once they run. A running sandbox is paused
|
|
@@ -229,6 +252,13 @@ export declare class Process {
|
|
|
229
252
|
info: ProcessInfo;
|
|
230
253
|
constructor(t: Transport, sandboxId: string, info: ProcessInfo);
|
|
231
254
|
get id(): string;
|
|
255
|
+
/** The command line as the sandbox reports it. */
|
|
256
|
+
get command(): string;
|
|
257
|
+
/** As of the last answer: spawn, `wait()` or `refresh()`. */
|
|
258
|
+
get state(): ProcessInfo["state"];
|
|
259
|
+
/** null while it runs, and when it was killed or timed out. */
|
|
260
|
+
get exitCode(): number | null;
|
|
261
|
+
toJSON(): ProcessInfo;
|
|
232
262
|
/** Every output event from the start (or `cursor`) until exit. */
|
|
233
263
|
output(options?: {
|
|
234
264
|
cursor?: number;
|
package/dist/sandbox.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { KEEP_ALIVE_MARGIN_SECONDS, STREAMED_EXEC_TIMEOUT_MS, WAIT_FOR_TIMEOUT_SECONDS, } from "./api-defaults.js";
|
|
2
|
-
import { CommandError, ConnectionError, NotFoundError, RuntimeError } from "./errors.js";
|
|
2
|
+
import { CommandError, ConnectionError, DELIBERATE, errorFor, NotFoundError, RateLimitError, RuntimeError, ServiceUnavailableError, } from "./errors.js";
|
|
3
3
|
import { Page } from "./page.js";
|
|
4
4
|
import { sandboxWatches, watchDirectory, } from "./products/watch.js";
|
|
5
5
|
import { Transport } from "./transport.js";
|
|
6
6
|
import { sandboxFactories } from "./products/index.js";
|
|
7
7
|
import { Tunnel } from "./tunnel.js";
|
|
8
|
+
import { pause } from "./wait.js";
|
|
9
|
+
/** How long snapshot() waits, pause and capture together, unless told. */
|
|
10
|
+
const SNAPSHOT_DEADLINE_MS = 10 * 60_000;
|
|
8
11
|
const CHUNK = 1_048_576;
|
|
9
12
|
/** Chunks of a large write in flight at once. Each chunk's reply waits on the
|
|
10
13
|
* API and the guest, and the link idles while every chunk in flight waits:
|
|
@@ -380,7 +383,8 @@ export class Sandbox {
|
|
|
380
383
|
this.#keepAlive?.();
|
|
381
384
|
return deleteSandbox(this.#t, this.id, options);
|
|
382
385
|
}
|
|
383
|
-
/** Carries on a paused sandbox;
|
|
386
|
+
/** Carries on a paused sandbox; for one with a time limit,
|
|
387
|
+
* `timeoutSeconds` is its new one. */
|
|
384
388
|
async wake(options = {}) {
|
|
385
389
|
const { timeoutSeconds, ...rest } = options;
|
|
386
390
|
try {
|
|
@@ -391,13 +395,15 @@ export class Sandbox {
|
|
|
391
395
|
// lease asked for, the refusal stands: it was not given.
|
|
392
396
|
if (!(error instanceof RuntimeError) || error.code !== "not_paused" || timeoutSeconds)
|
|
393
397
|
throw error;
|
|
394
|
-
await this.refresh(
|
|
398
|
+
await this.refresh(requestOnly(rest));
|
|
395
399
|
if (this.state !== "running" && this.state !== "starting")
|
|
396
400
|
throw error;
|
|
397
401
|
return this;
|
|
398
402
|
}
|
|
399
403
|
}
|
|
400
|
-
/** More time before
|
|
404
|
+
/** More time before its time limit ends (at most an hour ahead of now). A
|
|
405
|
+
* sandbox with no time limit needs none: the call answers at once and
|
|
406
|
+
* changes nothing. */
|
|
401
407
|
async extend(seconds, options = {}) {
|
|
402
408
|
return this.#lifecycle("extend", { ...options, wait: false }, { seconds });
|
|
403
409
|
}
|
|
@@ -411,27 +417,51 @@ export class Sandbox {
|
|
|
411
417
|
* its id, /workspace (its home: dotfiles, pip --user and npm -g installs),
|
|
412
418
|
* volumes, environment, name and previews. Its processes restart, and
|
|
413
419
|
* everything else on its old disk (sudo installs, apt packages, /etc) is
|
|
414
|
-
* lost
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
|
|
419
|
-
|
|
420
|
+
* lost; snapshot it first to keep everything. A running sandbox is paused
|
|
421
|
+
* first. A switch that fails is undone (`switch_undone`), the sandbox on
|
|
422
|
+
* its old image with nothing lost. Charged as a wake. `keep` is optional:
|
|
423
|
+
* "workspace" is the only value, and it is sent either way, so an API from
|
|
424
|
+
* before it was optional accepts the call too. */
|
|
425
|
+
async switchImage(image, options = {}) {
|
|
426
|
+
const { keep = "workspace", ...rest } = options;
|
|
420
427
|
this.#keep(await this.#t.json({
|
|
421
428
|
method: "POST",
|
|
422
429
|
path: `/v1/sandboxes/${enc(this.id)}:switch-image`,
|
|
423
430
|
body: { image, keep },
|
|
424
431
|
// A pause, two boots and the copy between them.
|
|
425
432
|
wait: 120,
|
|
426
|
-
...
|
|
433
|
+
...requestOnly(rest),
|
|
427
434
|
}));
|
|
428
435
|
return this;
|
|
429
436
|
}
|
|
430
|
-
/**
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
437
|
+
/** Gives it more or fewer vCPUs or more or less memory by a restart: its id,
|
|
438
|
+
* whole disk, volumes, environment, name and previews stay, and its
|
|
439
|
+
* programs stop (snapshot it first to keep its memory too). A running sandbox is paused, or stopped if it is
|
|
440
|
+
* persistent, and comes back running at the new size on the same server; a
|
|
441
|
+
* paused or stopped one is started. Memory bills on the new size from then.
|
|
442
|
+
* Refused, with nothing changed, when the server has no room
|
|
443
|
+
* (`no_capacity`), above your quota or the trial's 2 vCPU and 4 GiB, or
|
|
444
|
+
* while a snapshot or fork of it is being taken. `restart` is optional:
|
|
445
|
+
* true is the only value, and it is sent either way, so an API from before
|
|
446
|
+
* it was optional accepts the call too. */
|
|
447
|
+
async resize(size, options = {}) {
|
|
448
|
+
const { restart = true, ...rest } = options;
|
|
449
|
+
this.#keep(await this.#t.json({
|
|
450
|
+
method: "POST",
|
|
451
|
+
path: `/v1/sandboxes/${enc(this.id)}:resize`,
|
|
452
|
+
body: { ...size, restart },
|
|
453
|
+
// A pause or stop and a cold boot.
|
|
454
|
+
wait: 120,
|
|
455
|
+
...requestOnly(rest),
|
|
456
|
+
}));
|
|
457
|
+
return this;
|
|
458
|
+
}
|
|
459
|
+
/** Keeps a sandbox with a time limit running past it, in the background,
|
|
460
|
+
* until stop() or the returned function ends it: every `everySeconds` (60)
|
|
461
|
+
* it extends the limit so that `marginSeconds` (600) remain, never more
|
|
462
|
+
* than the hour ahead the API allows. A sandbox with no time limit
|
|
463
|
+
* (`endsAt` null) needs none, and is only watched. Running time is billed
|
|
464
|
+
* as it is used. A paused sandbox is left paused (a request wakes it,
|
|
435
465
|
* unless autoWake is off); a stopped one ends the loop. It does not keep a
|
|
436
466
|
* Node or Bun process alive by itself. */
|
|
437
467
|
keepAlive(options = {}) {
|
|
@@ -454,8 +484,9 @@ export class Sandbox {
|
|
|
454
484
|
await this.refresh();
|
|
455
485
|
if (this.state === "stopped" || this.state === "stopping")
|
|
456
486
|
return end();
|
|
457
|
-
|
|
458
|
-
|
|
487
|
+
// No time limit: nothing to extend (an older server sends no endsAt).
|
|
488
|
+
if (this.state === "running" && this.info.endsAt !== null) {
|
|
489
|
+
const left = (Date.parse(this.info.endsAt ?? this.info.expiresAt) - Date.now()) / 1000;
|
|
459
490
|
const need = Math.ceil(margin - left);
|
|
460
491
|
if (need >= 1)
|
|
461
492
|
await this.extend(Math.min(3600, need));
|
|
@@ -479,16 +510,34 @@ export class Sandbox {
|
|
|
479
510
|
}
|
|
480
511
|
/** Keeps this sandbox's whole machine (files, memory, running processes) as
|
|
481
512
|
* a snapshot to start new sandboxes from. A running sandbox is paused for
|
|
482
|
-
* the moment it takes, then woken; a paused one stays paused.
|
|
513
|
+
* the moment it takes, then woken; a paused one stays paused. Past
|
|
514
|
+
* `timeoutMs` (ten minutes unless given) mid-capture, it stays paused and
|
|
515
|
+
* the error names the snapshot. */
|
|
483
516
|
async snapshot(options = {}) {
|
|
484
517
|
const { idempotencyKey, signal, timeoutMs, ...body } = options;
|
|
485
518
|
if (body.mode !== undefined && body.mode !== "memory" && body.mode !== "disk")
|
|
486
519
|
throw new TypeError("Snapshot mode must be memory or disk.");
|
|
487
520
|
signal?.throwIfAborted();
|
|
488
|
-
|
|
521
|
+
// One minute, whatever `timeoutMs` said, was too short: pausing a busy
|
|
522
|
+
// 16 vCPU, 32 GiB sandbox writes its memory out first (4 October 2026).
|
|
523
|
+
const until = performance.now() + (timeoutMs && timeoutMs > 0 ? timeoutMs : SNAPSHOT_DEADLINE_MS);
|
|
524
|
+
let running = false;
|
|
525
|
+
/** The capture in progress, once the server has one. */
|
|
526
|
+
let capturing;
|
|
527
|
+
let late = false;
|
|
489
528
|
const request = () => {
|
|
490
529
|
signal?.throwIfAborted();
|
|
491
530
|
const left = Math.ceil(until - performance.now());
|
|
531
|
+
if (left <= 0 && capturing) {
|
|
532
|
+
late = true;
|
|
533
|
+
throw new RuntimeError({
|
|
534
|
+
code: "snapshot_timeout",
|
|
535
|
+
status: 0,
|
|
536
|
+
message: `Snapshot ${capturing} was still capturing at the deadline.${running ? " The sandbox stays paused until it ends: waking it now would fail the capture." : ""}`,
|
|
537
|
+
hint: `Wait until runtime.snapshots.get("${capturing}") is ready${running ? ", then wake the sandbox" : ""}, or pass a longer timeoutMs.`,
|
|
538
|
+
details: { snapshotId: capturing, sourceSandboxId: this.id },
|
|
539
|
+
});
|
|
540
|
+
}
|
|
492
541
|
if (left <= 0)
|
|
493
542
|
throw new RuntimeError({
|
|
494
543
|
code: "snapshot_timeout",
|
|
@@ -505,7 +554,7 @@ export class Sandbox {
|
|
|
505
554
|
await this.waitFor("running", request());
|
|
506
555
|
else if (this.state === "pausing")
|
|
507
556
|
await this.waitFor("paused", request());
|
|
508
|
-
|
|
557
|
+
running = this.state === "running";
|
|
509
558
|
// Check before entering cleanup: an abort during refresh must not pause or wake.
|
|
510
559
|
const pauseOptions = running ? request() : undefined;
|
|
511
560
|
let captured;
|
|
@@ -526,14 +575,8 @@ export class Sandbox {
|
|
|
526
575
|
// Prefer: wait is bounded on the server. A capture still in progress
|
|
527
576
|
// must keep its source paused, or the worker refuses it after we wake.
|
|
528
577
|
while (snapshot.state === "capturing") {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
throw new RuntimeError({
|
|
532
|
-
code: "snapshot_timeout",
|
|
533
|
-
status: 0,
|
|
534
|
-
message: "Snapshot capture did not finish within one minute.",
|
|
535
|
-
details: { snapshotId: snapshot.id },
|
|
536
|
-
});
|
|
578
|
+
capturing = snapshot.id;
|
|
579
|
+
request();
|
|
537
580
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
538
581
|
snapshot = await this.#t.json({
|
|
539
582
|
method: "GET",
|
|
@@ -560,8 +603,13 @@ export class Sandbox {
|
|
|
560
603
|
catch (error) {
|
|
561
604
|
failed = true;
|
|
562
605
|
primary = error;
|
|
606
|
+
// A request cut off by the deadline mid-capture is late all the same.
|
|
607
|
+
if (capturing && performance.now() >= until)
|
|
608
|
+
late = true;
|
|
563
609
|
}
|
|
564
|
-
|
|
610
|
+
// A capture still running needs its source paused: the worker fails one
|
|
611
|
+
// whose source woke. The timeout says so and leaves the wake to the caller.
|
|
612
|
+
if (running && !late) {
|
|
565
613
|
try {
|
|
566
614
|
await this.wake();
|
|
567
615
|
}
|
|
@@ -596,11 +644,7 @@ export class Sandbox {
|
|
|
596
644
|
path: `/v1/sandboxes/${enc(this.id)}:fork`,
|
|
597
645
|
body,
|
|
598
646
|
wait: 60,
|
|
599
|
-
...
|
|
600
|
-
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
601
|
-
...(signal ? { signal } : {}),
|
|
602
|
-
...(timeoutMs ? { timeoutMs } : {}),
|
|
603
|
-
}),
|
|
647
|
+
...requestOnly({ idempotencyKey, signal, timeoutMs }),
|
|
604
648
|
});
|
|
605
649
|
const sandboxes = reply.sandboxes.map((info) => new Sandbox(this.#t, info));
|
|
606
650
|
return options.count === undefined ? sandboxes[0] : sandboxes;
|
|
@@ -615,7 +659,7 @@ export class Sandbox {
|
|
|
615
659
|
path: `/v1/sandboxes/${enc(this.id)}:${verb}`,
|
|
616
660
|
body,
|
|
617
661
|
wait: options.wait === false ? 0 : 60,
|
|
618
|
-
...
|
|
662
|
+
...requestOnly(options),
|
|
619
663
|
}));
|
|
620
664
|
return this;
|
|
621
665
|
}
|
|
@@ -639,13 +683,10 @@ class OutputCursor {
|
|
|
639
683
|
this.cursor = cursor;
|
|
640
684
|
}
|
|
641
685
|
*pass(event) {
|
|
686
|
+
// As the status the API would have answered it, so a passing failure (a
|
|
687
|
+
// host restarting) is followed again rather than ending the read.
|
|
642
688
|
if (event.type === "error")
|
|
643
|
-
throw
|
|
644
|
-
message: event.error.message,
|
|
645
|
-
code: event.error.code,
|
|
646
|
-
status: 0,
|
|
647
|
-
...(event.error.requestId ? { requestId: event.error.requestId } : {}),
|
|
648
|
-
});
|
|
689
|
+
throw errorFor(event.error.status ?? 0, event);
|
|
649
690
|
if (event.type === "truncated")
|
|
650
691
|
this.cursor = Math.max(this.cursor, event.resumeAt);
|
|
651
692
|
if (event.type === "stdout" || event.type === "stderr") {
|
|
@@ -676,11 +717,20 @@ function processBytes(encoded) {
|
|
|
676
717
|
});
|
|
677
718
|
}
|
|
678
719
|
}
|
|
679
|
-
/** A stream cut off by the network
|
|
680
|
-
*
|
|
720
|
+
/** A stream cut off by the network, or refused by a failure that passes by
|
|
721
|
+
* itself (a busy API, a 429, a host restarting), rather than refused for good:
|
|
722
|
+
* worth reconnecting to from the cursor. */
|
|
681
723
|
function cutOff(error) {
|
|
682
|
-
return !(error instanceof RuntimeError) ||
|
|
724
|
+
return (!(error instanceof RuntimeError) ||
|
|
725
|
+
error instanceof ConnectionError ||
|
|
726
|
+
((error instanceof RateLimitError || error instanceof ServiceUnavailableError) &&
|
|
727
|
+
[429, 502, 503, 504].includes(error.status) &&
|
|
728
|
+
!DELIBERATE.has(error.code)));
|
|
683
729
|
}
|
|
730
|
+
/** How long a follow keeps reconnecting while every attempt fails. A command
|
|
731
|
+
* runs for hours whether or not anyone reads it; a few minutes of a deploy, a
|
|
732
|
+
* network blip or a busy API must not end the read of it. */
|
|
733
|
+
const FOLLOW_OUTAGE_MS = 300_000;
|
|
684
734
|
/** A streamed command keeps all its output unless the reader fell more than the
|
|
685
735
|
* process's output buffer behind, which the server marks with a "truncated"
|
|
686
736
|
* event. That event does not say which stream lost bytes, so both flags carry
|
|
@@ -688,6 +738,8 @@ function cutOff(error) {
|
|
|
688
738
|
function truncation(dropped) {
|
|
689
739
|
return { stdoutTruncated: dropped, stderrTruncated: dropped };
|
|
690
740
|
}
|
|
741
|
+
/** A command's request options: its `timeoutMs` is the command's own limit
|
|
742
|
+
* in the guest, so the call is given a minute more for the answer. */
|
|
691
743
|
function pick(options) {
|
|
692
744
|
return {
|
|
693
745
|
...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
|
|
@@ -695,6 +747,15 @@ function pick(options) {
|
|
|
695
747
|
...(options.timeoutMs ? { timeoutMs: options.timeoutMs + 60_000 } : {}),
|
|
696
748
|
};
|
|
697
749
|
}
|
|
750
|
+
/** Any other call's request options, as the caller gave them: `timeoutMs` is
|
|
751
|
+
* the call's whole deadline, and 0 turns it off. */
|
|
752
|
+
function requestOnly(options) {
|
|
753
|
+
return {
|
|
754
|
+
...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
|
|
755
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
756
|
+
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
|
|
757
|
+
};
|
|
758
|
+
}
|
|
698
759
|
/** Sessions: a token your backend makes and hands to your own frontend, which
|
|
699
760
|
* then runs commands, reads and writes files and reaches previews in this one
|
|
700
761
|
* sandbox directly, without proxying through your servers. A session cannot
|
|
@@ -783,12 +844,19 @@ export class Processes {
|
|
|
783
844
|
/** Output events from `cursor` until the process exits, across stream slices. */
|
|
784
845
|
async *follow(processId, options = {}) {
|
|
785
846
|
const read = new OutputCursor(options.cursor ?? 0);
|
|
786
|
-
// Reconnections in a row that
|
|
787
|
-
// closing without output or an exit is given up, not followed
|
|
847
|
+
// Reconnections in a row that closed cleanly with nothing: a stream that
|
|
848
|
+
// keeps closing without output or an exit is given up, not followed
|
|
849
|
+
// forever.
|
|
788
850
|
let idle = 0;
|
|
851
|
+
// Since when every reconnection has failed (a cut, a busy API, a host
|
|
852
|
+
// restarting). The command runs on regardless, so these are waited out
|
|
853
|
+
// for FOLLOW_OUTAGE_MS, with backoff, before the follow gives up.
|
|
854
|
+
let failingSince;
|
|
855
|
+
let failures = 0;
|
|
789
856
|
for (;;) {
|
|
790
857
|
const before = read.cursor;
|
|
791
858
|
let resumed = false;
|
|
859
|
+
let failure;
|
|
792
860
|
try {
|
|
793
861
|
for await (const event of this.t.events({
|
|
794
862
|
method: "GET",
|
|
@@ -810,8 +878,28 @@ export class Processes {
|
|
|
810
878
|
catch (error) {
|
|
811
879
|
if (options.signal?.aborted || !cutOff(error))
|
|
812
880
|
throw error;
|
|
881
|
+
failure = error;
|
|
882
|
+
}
|
|
883
|
+
if (resumed || read.cursor > before) {
|
|
884
|
+
idle = failures = 0;
|
|
885
|
+
failingSince = undefined;
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
if (failure === undefined)
|
|
889
|
+
idle++;
|
|
890
|
+
else {
|
|
891
|
+
failingSince ??= performance.now();
|
|
892
|
+
if (performance.now() - failingSince >= FOLLOW_OUTAGE_MS)
|
|
893
|
+
throw new ConnectionError({
|
|
894
|
+
message: `Lost the output stream of process ${processId} for ${FOLLOW_OUTAGE_MS / 60_000} minutes; the command may still be running.`,
|
|
895
|
+
code: "connection_error",
|
|
896
|
+
status: 0,
|
|
897
|
+
hint: `Follow it again: runtime sandbox logs ${this.sandboxId} ${processId} -f`,
|
|
898
|
+
details: { sandboxId: this.sandboxId, processId },
|
|
899
|
+
cause: failure,
|
|
900
|
+
});
|
|
901
|
+
await pause(Math.min(5000, 50 * 2 ** failures++), options.signal);
|
|
813
902
|
}
|
|
814
|
-
idle = resumed || read.cursor > before ? 0 : idle + 1;
|
|
815
903
|
if (idle > 3)
|
|
816
904
|
throw new ConnectionError({
|
|
817
905
|
message: `The output stream of process ${processId} keeps closing before it ends.`,
|
|
@@ -839,6 +927,26 @@ export class Process {
|
|
|
839
927
|
get id() {
|
|
840
928
|
return this.info.id;
|
|
841
929
|
}
|
|
930
|
+
/** The command line as the sandbox reports it. */
|
|
931
|
+
get command() {
|
|
932
|
+
return this.info.command;
|
|
933
|
+
}
|
|
934
|
+
/** As of the last answer: spawn, `wait()` or `refresh()`. */
|
|
935
|
+
get state() {
|
|
936
|
+
return this.info.state;
|
|
937
|
+
}
|
|
938
|
+
/** null while it runs, and when it was killed or timed out. */
|
|
939
|
+
get exitCode() {
|
|
940
|
+
return this.info.exitCode;
|
|
941
|
+
}
|
|
942
|
+
toJSON() {
|
|
943
|
+
return this.info;
|
|
944
|
+
}
|
|
945
|
+
/** console.log shows what the process is, not the client inside it. */
|
|
946
|
+
[Symbol.for("nodejs.util.inspect.custom")](_depth, options, show) {
|
|
947
|
+
const { id, sandboxId, command, state, exitCode } = this;
|
|
948
|
+
return `Process ${show({ id, sandboxId, command, state, exitCode }, options)}`;
|
|
949
|
+
}
|
|
842
950
|
/** Every output event from the start (or `cursor`) until exit. */
|
|
843
951
|
output(options = {}) {
|
|
844
952
|
return new Processes(this.t, this.sandboxId).follow(this.id, options);
|
|
@@ -884,6 +992,12 @@ export class Process {
|
|
|
884
992
|
if (event.type === "exit")
|
|
885
993
|
exit = event;
|
|
886
994
|
}
|
|
995
|
+
if (exit)
|
|
996
|
+
this.info = {
|
|
997
|
+
...this.info,
|
|
998
|
+
state: exit.state,
|
|
999
|
+
exitCode: exit.exitCode,
|
|
1000
|
+
};
|
|
887
1001
|
return {
|
|
888
1002
|
exitCode: exit?.exitCode ?? null,
|
|
889
1003
|
...out,
|
package/dist/tar.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** One entry's header: the ustar block, after the long-name and long-link
|
|
2
|
+
* entries it needs. A whole number of 512-byte blocks. */
|
|
1
3
|
export declare function tarHeader(name: string, size: number, mode: number, type: "0" | "5" | "2", link?: string, mtime?: number): Uint8Array;
|
|
2
4
|
export declare function packDirectory(root: string): Promise<Uint8Array>;
|
|
3
5
|
/** Unpacks a gzipped tar that arrives as a stream into `target`, refusing any
|
package/dist/tar.js
CHANGED
|
@@ -1,25 +1,35 @@
|
|
|
1
|
-
import { chmod, lstat, mkdir, mkdtemp, open, readdir, readFile, readlink, realpath, rename, rm, symlink, unlink, } from "node:fs/promises";
|
|
1
|
+
import { chmod, copyFile, link as hardLink, lstat, mkdir, mkdtemp, open, readdir, readFile, readlink, realpath, rename, rm, symlink, unlink, } from "node:fs/promises";
|
|
2
2
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { Readable } from "node:stream";
|
|
4
4
|
import { createGunzip, gzipSync } from "node:zlib";
|
|
5
5
|
import { RuntimeError } from "./errors.js";
|
|
6
|
-
/* A small
|
|
7
|
-
SDK needs no dependency. Regular files, directories
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
prefix
|
|
19
|
-
|
|
6
|
+
/* A small tar writer and reader for directory uploads and downloads, so the
|
|
7
|
+
SDK needs no dependency. Regular files, directories, symbolic links and,
|
|
8
|
+
reading, hard links. A name that does not fit the ustar name and prefix
|
|
9
|
+
fields, or a link target longer than 100 bytes, travels in a GNU long-name
|
|
10
|
+
or long-link entry, which GNU tar, BusyBox tar and every SDK's reader
|
|
11
|
+
take; nothing is cut short. */
|
|
12
|
+
const encode = (value) => new TextEncoder().encode(value);
|
|
13
|
+
/** The ustar prefix and name of `name`, split at a slash so that each fits
|
|
14
|
+
* its field in bytes, or null when no split fits. */
|
|
15
|
+
function ustarName(name) {
|
|
16
|
+
const bytes = encode(name);
|
|
17
|
+
if (bytes.length <= 100)
|
|
18
|
+
return { prefix: new Uint8Array(), short: bytes };
|
|
19
|
+
for (let split = name.lastIndexOf("/"); split > 0; split = name.lastIndexOf("/", split - 1)) {
|
|
20
|
+
const prefix = encode(name.slice(0, split));
|
|
21
|
+
const short = encode(name.slice(split + 1));
|
|
22
|
+
if (short.length > 100)
|
|
23
|
+
return null;
|
|
24
|
+
if (prefix.length <= 155)
|
|
25
|
+
return { prefix, short };
|
|
20
26
|
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
function headerBlock(short, prefix, size, mode, type, link, mtime) {
|
|
30
|
+
const block = new Uint8Array(512);
|
|
21
31
|
const put = (offset, value, length) => {
|
|
22
|
-
const data = typeof value === "string" ?
|
|
32
|
+
const data = typeof value === "string" ? encode(value) : value;
|
|
23
33
|
block.set(data.subarray(0, length), offset);
|
|
24
34
|
};
|
|
25
35
|
const octal = (value, length) => value.toString(8).padStart(length - 1, "0") + "\0";
|
|
@@ -40,6 +50,29 @@ export function tarHeader(name, size, mode, type, link = "", mtime = 0) {
|
|
|
40
50
|
put(148, sum.toString(8).padStart(6, "0") + "\0 ", 8);
|
|
41
51
|
return block;
|
|
42
52
|
}
|
|
53
|
+
/** A GNU long-name ("L") or long-link ("K") entry carrying `value`. */
|
|
54
|
+
function longEntry(type, value) {
|
|
55
|
+
const size = value.length + 1;
|
|
56
|
+
const out = new Uint8Array(512 + Math.ceil(size / 512) * 512);
|
|
57
|
+
out.set(headerBlock(encode("././@LongLink"), new Uint8Array(), size, 0o644, type, new Uint8Array(), 0));
|
|
58
|
+
out.set(value, 512);
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
/** One entry's header: the ustar block, after the long-name and long-link
|
|
62
|
+
* entries it needs. A whole number of 512-byte blocks. */
|
|
63
|
+
export function tarHeader(name, size, mode, type, link = "", mtime = 0) {
|
|
64
|
+
const parts = [];
|
|
65
|
+
let fitted = ustarName(name);
|
|
66
|
+
if (!fitted) {
|
|
67
|
+
parts.push(longEntry("L", encode(name)));
|
|
68
|
+
fitted = { prefix: new Uint8Array(), short: encode(name).subarray(0, 100) };
|
|
69
|
+
}
|
|
70
|
+
const target = encode(link);
|
|
71
|
+
if (target.length > 100)
|
|
72
|
+
parts.push(longEntry("K", target));
|
|
73
|
+
parts.push(headerBlock(fitted.short, fitted.prefix, size, mode, type, target, mtime));
|
|
74
|
+
return parts.length === 1 ? parts[0] : new Uint8Array(Buffer.concat(parts));
|
|
75
|
+
}
|
|
43
76
|
export async function packDirectory(root) {
|
|
44
77
|
const parts = [];
|
|
45
78
|
const base = resolve(root);
|
|
@@ -352,8 +385,15 @@ async function unpackEntries(bytes, root, signal) {
|
|
|
352
385
|
links.push({ destination, link, name });
|
|
353
386
|
await skip();
|
|
354
387
|
}
|
|
388
|
+
else if (type === "1") {
|
|
389
|
+
await hardLinkEntry(root, destination, link, name);
|
|
390
|
+
await skip();
|
|
391
|
+
}
|
|
355
392
|
else if (type === "0" || type === "\0" || type === "7") {
|
|
356
393
|
await mkdir(dirname(destination), { recursive: true });
|
|
394
|
+
// A name already unpacked may share its file with a hard link; writing
|
|
395
|
+
// through it would change the link's copy too.
|
|
396
|
+
await unlink(destination).catch(() => undefined);
|
|
357
397
|
const file = await open(destination, "w");
|
|
358
398
|
try {
|
|
359
399
|
for await (const part of bytes.take(size)) {
|
|
@@ -378,6 +418,24 @@ async function unpackEntries(bytes, root, signal) {
|
|
|
378
418
|
await skip();
|
|
379
419
|
}
|
|
380
420
|
}
|
|
421
|
+
/** A second name for a file the archive already carried: tar writes the
|
|
422
|
+
* first name as a file and every other as a hard link to it. The link names
|
|
423
|
+
* a plain file unpacked earlier in this archive, or the unpack fails; it
|
|
424
|
+
* never reaches outside, through a link or ahead. */
|
|
425
|
+
async function hardLinkEntry(root, destination, link, name) {
|
|
426
|
+
const named = link.replace(/^(\.\/)+/, "");
|
|
427
|
+
const source = resolve(root, named);
|
|
428
|
+
if (!named || isAbsolute(named) || source === root || !source.startsWith(root + sep))
|
|
429
|
+
throw new Error(`Refusing an archive hard link that leads outside the target: ${name} -> ${link}`);
|
|
430
|
+
await assertPlain(root, source, named);
|
|
431
|
+
if (!(await lstat(source).catch(() => undefined))?.isFile())
|
|
432
|
+
throw new Error(`Refusing an archive hard link to a file it does not carry: ${name} -> ${link}`);
|
|
433
|
+
if (source === destination)
|
|
434
|
+
return;
|
|
435
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
436
|
+
await unlink(destination).catch(() => undefined);
|
|
437
|
+
await hardLink(source, destination).catch(() => copyFile(source, destination));
|
|
438
|
+
}
|
|
381
439
|
/** Moves what was unpacked in `from` into `to`, merging with what is there
|
|
382
440
|
* and replacing files of the same name, never through a link in `to`. */
|
|
383
441
|
async function merge(from, to, root, signal) {
|