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/sandbox.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { KEEP_ALIVE_MARGIN_SECONDS, STREAMED_EXEC_TIMEOUT_MS, WAIT_FOR_TIMEOUT_SECONDS, } from "./api-defaults.js";
|
|
1
2
|
import { CommandError, ConnectionError, NotFoundError, RuntimeError } from "./errors.js";
|
|
2
3
|
import { Page } from "./page.js";
|
|
3
4
|
import { sandboxWatches, watchDirectory, } from "./products/watch.js";
|
|
@@ -12,6 +13,15 @@ const CHUNK = 1_048_576;
|
|
|
12
13
|
* connections at once. */
|
|
13
14
|
const PARALLEL = 8;
|
|
14
15
|
const enc = (id) => encodeURIComponent(id);
|
|
16
|
+
/** DELETE /v1/sandboxes/{id}, for `sandbox.delete()` and
|
|
17
|
+
* `runtime.sandboxes.delete(id)`. */
|
|
18
|
+
export function deleteSandbox(t, id, options = {}) {
|
|
19
|
+
return t.json({
|
|
20
|
+
method: "DELETE",
|
|
21
|
+
path: `/v1/sandboxes/${enc(id)}`,
|
|
22
|
+
...options,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
15
25
|
/* Web APIs only, so the sandbox's commands, processes and files work in a
|
|
16
26
|
browser with a session token (Sandbox.fromSession): no Buffer, no
|
|
17
27
|
node:crypto. */
|
|
@@ -85,6 +95,13 @@ export class Sandbox {
|
|
|
85
95
|
});
|
|
86
96
|
return new Sandbox(transport, { id: session.sandboxId });
|
|
87
97
|
}
|
|
98
|
+
/** The API's latest answer. `start`, the report of an image's start
|
|
99
|
+
* command, is the create's alone: the API keeps no record of it, so a
|
|
100
|
+
* later read of this sandbox keeps it here. */
|
|
101
|
+
#keep(info) {
|
|
102
|
+
const start = this.#info?.start;
|
|
103
|
+
this.#info = info.start === undefined && start !== undefined ? { ...info, start } : info;
|
|
104
|
+
}
|
|
88
105
|
/** What the API last said about this sandbox. `refresh()` asks again. */
|
|
89
106
|
get info() {
|
|
90
107
|
return this.#info;
|
|
@@ -93,21 +110,24 @@ export class Sandbox {
|
|
|
93
110
|
return this.#info.state;
|
|
94
111
|
}
|
|
95
112
|
async refresh(options = {}) {
|
|
96
|
-
this.#
|
|
113
|
+
this.#keep(await this.#t.json({
|
|
97
114
|
method: "GET",
|
|
98
115
|
path: `/v1/sandboxes/${enc(this.id)}`,
|
|
99
116
|
...options,
|
|
100
|
-
});
|
|
117
|
+
}));
|
|
101
118
|
return this;
|
|
102
119
|
}
|
|
103
120
|
/** Waits (server-side, no polling) until the sandbox reaches `state`. */
|
|
104
121
|
async waitFor(state, options = {}) {
|
|
105
|
-
this.#
|
|
122
|
+
this.#keep(await this.#t.json({
|
|
106
123
|
method: "GET",
|
|
107
124
|
path: `/v1/sandboxes/${enc(this.id)}`,
|
|
108
|
-
query: {
|
|
125
|
+
query: {
|
|
126
|
+
waitFor: state,
|
|
127
|
+
timeoutSeconds: options.timeoutSeconds ?? WAIT_FOR_TIMEOUT_SECONDS,
|
|
128
|
+
},
|
|
109
129
|
...options,
|
|
110
|
-
});
|
|
130
|
+
}));
|
|
111
131
|
return this;
|
|
112
132
|
}
|
|
113
133
|
/** Runs a command and returns its exit code and output. A string runs under
|
|
@@ -136,7 +156,7 @@ export class Sandbox {
|
|
|
136
156
|
processId = event.processId;
|
|
137
157
|
else if (event.type === "stdout" || event.type === "stderr") {
|
|
138
158
|
out[event.type] += event.data;
|
|
139
|
-
(event.type === "stdout" ? options.onStdout : options.onStderr)?.(event.data);
|
|
159
|
+
await (event.type === "stdout" ? options.onStdout : options.onStderr)?.(event.data);
|
|
140
160
|
}
|
|
141
161
|
else if (event.type === "truncated")
|
|
142
162
|
dropped = true;
|
|
@@ -206,9 +226,12 @@ export class Sandbox {
|
|
|
206
226
|
const events = this.#t.events({
|
|
207
227
|
method: "POST",
|
|
208
228
|
path: `/v1/sandboxes/${enc(this.id)}:exec`,
|
|
209
|
-
body: {
|
|
229
|
+
body: {
|
|
230
|
+
...commandBody(command, { timeoutMs: STREAMED_EXEC_TIMEOUT_MS, ...options }),
|
|
231
|
+
stream: true,
|
|
232
|
+
},
|
|
210
233
|
...pick(options),
|
|
211
|
-
timeoutMs: options.timeoutMs === undefined ?
|
|
234
|
+
timeoutMs: options.timeoutMs === undefined ? STREAMED_EXEC_TIMEOUT_MS : options.timeoutMs + 60_000,
|
|
212
235
|
});
|
|
213
236
|
try {
|
|
214
237
|
for await (const event of events) {
|
|
@@ -245,7 +268,7 @@ export class Sandbox {
|
|
|
245
268
|
* once. `stdin: "pipe"` keeps its input open for write(); `pty` gives it a
|
|
246
269
|
* terminal. */
|
|
247
270
|
async spawn(command, options = {}) {
|
|
248
|
-
const { stdin, pty, ...rest } = options;
|
|
271
|
+
const { stdin, pty, outputEncoding, ...rest } = options;
|
|
249
272
|
const info = await this.#t.json({
|
|
250
273
|
method: "POST",
|
|
251
274
|
path: `/v1/sandboxes/${enc(this.id)}/processes`,
|
|
@@ -253,6 +276,7 @@ export class Sandbox {
|
|
|
253
276
|
...commandBody(command, rest),
|
|
254
277
|
...(stdin === "pipe" ? { stdinMode: "pipe" } : stdin === undefined ? {} : { stdin }),
|
|
255
278
|
...(pty ? { pty } : {}),
|
|
279
|
+
...(outputEncoding ? { outputEncoding } : {}),
|
|
256
280
|
},
|
|
257
281
|
...pick(rest),
|
|
258
282
|
});
|
|
@@ -304,6 +328,14 @@ export class Sandbox {
|
|
|
304
328
|
async pause(options = {}) {
|
|
305
329
|
return this.#lifecycle("pause", options);
|
|
306
330
|
}
|
|
331
|
+
/** Deletes it for good: stops it if it runs or is paused, deletes its disk
|
|
332
|
+
* and paused memory, revokes its previews and ports, and removes it from
|
|
333
|
+
* lists. Its snapshots, usage and audit entries stay. Deleting it again
|
|
334
|
+
* answers the same. */
|
|
335
|
+
async delete(options = {}) {
|
|
336
|
+
this.#keepAlive?.();
|
|
337
|
+
return deleteSandbox(this.#t, this.id, options);
|
|
338
|
+
}
|
|
307
339
|
/** Carries on a paused sandbox; `timeoutSeconds` is its new lease. */
|
|
308
340
|
async wake(options = {}) {
|
|
309
341
|
const { timeoutSeconds, ...rest } = options;
|
|
@@ -331,6 +363,26 @@ export class Sandbox {
|
|
|
331
363
|
async update(settings, options = {}) {
|
|
332
364
|
return this.#lifecycle("update", { ...options, wait: false }, { ...settings });
|
|
333
365
|
}
|
|
366
|
+
/** Moves it to another image (id, name, name:tag or name@version), keeping
|
|
367
|
+
* its id, /workspace (its home: dotfiles, pip --user and npm -g installs),
|
|
368
|
+
* volumes, environment, name and previews. Its processes restart, and
|
|
369
|
+
* everything else on its old disk (sudo installs, apt packages, /etc) is
|
|
370
|
+
* lost, which `keep: "workspace"` says you know; snapshot it first to keep
|
|
371
|
+
* everything. A running sandbox is paused first. A switch that fails is
|
|
372
|
+
* undone (`switch_undone`), the sandbox on its old image with nothing lost.
|
|
373
|
+
* Charged as a wake. */
|
|
374
|
+
async switchImage(image, options) {
|
|
375
|
+
const { keep, ...rest } = options;
|
|
376
|
+
this.#keep(await this.#t.json({
|
|
377
|
+
method: "POST",
|
|
378
|
+
path: `/v1/sandboxes/${enc(this.id)}:switch-image`,
|
|
379
|
+
body: { image, keep },
|
|
380
|
+
// A pause, two boots and the copy between them.
|
|
381
|
+
wait: 120,
|
|
382
|
+
...pick(rest),
|
|
383
|
+
}));
|
|
384
|
+
return this;
|
|
385
|
+
}
|
|
334
386
|
/** Keeps a running sandbox's lease ahead of now, in the background, until
|
|
335
387
|
* stop() or the returned function ends it: every `everySeconds` (60) it
|
|
336
388
|
* extends the lease so that `marginSeconds` (600) remain, never more than
|
|
@@ -341,7 +393,7 @@ export class Sandbox {
|
|
|
341
393
|
keepAlive(options = {}) {
|
|
342
394
|
this.#keepAlive?.();
|
|
343
395
|
const every = Math.max(10, options.everySeconds ?? 60) * 1000;
|
|
344
|
-
const margin = Math.min(3600, Math.max(60, options.marginSeconds ??
|
|
396
|
+
const margin = Math.min(3600, Math.max(60, options.marginSeconds ?? KEEP_ALIVE_MARGIN_SECONDS));
|
|
345
397
|
let ended = false;
|
|
346
398
|
let timer;
|
|
347
399
|
const end = () => {
|
|
@@ -386,29 +438,76 @@ export class Sandbox {
|
|
|
386
438
|
* the moment it takes, then woken; a paused one stays paused. */
|
|
387
439
|
async snapshot(options = {}) {
|
|
388
440
|
const { idempotencyKey, signal, timeoutMs, ...body } = options;
|
|
389
|
-
|
|
441
|
+
if (body.mode !== undefined && body.mode !== "memory" && body.mode !== "disk")
|
|
442
|
+
throw new TypeError("Snapshot mode must be memory or disk.");
|
|
443
|
+
signal?.throwIfAborted();
|
|
444
|
+
const until = performance.now() + Math.min(timeoutMs && timeoutMs > 0 ? timeoutMs : 60_000, 60_000);
|
|
445
|
+
const request = () => {
|
|
446
|
+
signal?.throwIfAborted();
|
|
447
|
+
const left = Math.ceil(until - performance.now());
|
|
448
|
+
if (left <= 0)
|
|
449
|
+
throw new RuntimeError({
|
|
450
|
+
code: "snapshot_timeout",
|
|
451
|
+
status: 0,
|
|
452
|
+
message: "Snapshot capture ran past its deadline.",
|
|
453
|
+
});
|
|
454
|
+
return { ...(signal ? { signal } : {}), timeoutMs: left };
|
|
455
|
+
};
|
|
456
|
+
await this.refresh(request());
|
|
390
457
|
// Straight after a fork or a wake the sandbox is still `resuming`, and
|
|
391
458
|
// after a pause still `pausing`: wait for where it is going, or a
|
|
392
459
|
// snapshot of it is refused as not paused (user lane, 23 September 2026).
|
|
393
460
|
if (this.state === "resuming" || this.state === "starting")
|
|
394
|
-
await this.waitFor("running");
|
|
461
|
+
await this.waitFor("running", request());
|
|
395
462
|
else if (this.state === "pausing")
|
|
396
|
-
await this.waitFor("paused");
|
|
463
|
+
await this.waitFor("paused", request());
|
|
397
464
|
const running = this.state === "running";
|
|
398
|
-
|
|
399
|
-
|
|
465
|
+
// Check before entering cleanup: an abort during refresh must not pause or wake.
|
|
466
|
+
const pauseOptions = running ? request() : undefined;
|
|
400
467
|
try {
|
|
401
|
-
|
|
468
|
+
if (running)
|
|
469
|
+
await this.pause(pauseOptions);
|
|
470
|
+
let snapshot = await this.#t.json({
|
|
402
471
|
method: "POST",
|
|
403
472
|
path: `/v1/sandboxes/${enc(this.id)}:snapshot`,
|
|
404
473
|
body,
|
|
405
474
|
wait: 10,
|
|
406
|
-
...
|
|
407
|
-
|
|
408
|
-
...(signal ? { signal } : {}),
|
|
409
|
-
...(timeoutMs ? { timeoutMs } : {}),
|
|
410
|
-
}),
|
|
475
|
+
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
476
|
+
...request(),
|
|
411
477
|
});
|
|
478
|
+
// Prefer: wait is bounded on the server. A capture still in progress
|
|
479
|
+
// must keep its source paused, or the worker refuses it after we wake.
|
|
480
|
+
while (snapshot.state === "capturing") {
|
|
481
|
+
signal?.throwIfAborted();
|
|
482
|
+
if (performance.now() >= until)
|
|
483
|
+
throw new RuntimeError({
|
|
484
|
+
code: "snapshot_timeout",
|
|
485
|
+
status: 0,
|
|
486
|
+
message: "Snapshot capture did not finish within one minute.",
|
|
487
|
+
details: { snapshotId: snapshot.id },
|
|
488
|
+
});
|
|
489
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
490
|
+
snapshot = await this.#t.json({
|
|
491
|
+
method: "GET",
|
|
492
|
+
path: `/v1/snapshots/${enc(snapshot.id)}`,
|
|
493
|
+
...request(),
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
if (snapshot.state !== "ready")
|
|
497
|
+
throw new RuntimeError({
|
|
498
|
+
code: "snapshot_failed",
|
|
499
|
+
status: 409,
|
|
500
|
+
message: snapshot.error ?? `Snapshot capture ended in state ${snapshot.state}.`,
|
|
501
|
+
details: { snapshotId: snapshot.id },
|
|
502
|
+
});
|
|
503
|
+
if (body.mode === "disk" && snapshot.mode !== "disk")
|
|
504
|
+
throw new RuntimeError({
|
|
505
|
+
code: "snapshot_mode_mismatch",
|
|
506
|
+
status: 409,
|
|
507
|
+
message: "The server did not confirm a disk-only snapshot.",
|
|
508
|
+
details: { snapshotId: snapshot.id },
|
|
509
|
+
});
|
|
510
|
+
return snapshot;
|
|
412
511
|
}
|
|
413
512
|
finally {
|
|
414
513
|
if (running)
|
|
@@ -436,13 +535,13 @@ export class Sandbox {
|
|
|
436
535
|
return this.#lifecycle("restart", options);
|
|
437
536
|
}
|
|
438
537
|
async #lifecycle(verb, options, body = {}) {
|
|
439
|
-
this.#
|
|
538
|
+
this.#keep(await this.#t.json({
|
|
440
539
|
method: "POST",
|
|
441
540
|
path: `/v1/sandboxes/${enc(this.id)}:${verb}`,
|
|
442
541
|
body,
|
|
443
542
|
wait: options.wait === false ? 0 : 60,
|
|
444
543
|
...pick(options),
|
|
445
|
-
});
|
|
544
|
+
}));
|
|
446
545
|
return this;
|
|
447
546
|
}
|
|
448
547
|
async [Symbol.asyncDispose]() {
|
|
@@ -481,11 +580,27 @@ class OutputCursor {
|
|
|
481
580
|
droppedBytes: event.offset - this.cursor,
|
|
482
581
|
resumeAt: event.offset,
|
|
483
582
|
};
|
|
484
|
-
this.cursor = Math.max(this.cursor, event.offset +
|
|
583
|
+
this.cursor = Math.max(this.cursor, event.offset +
|
|
584
|
+
(event.base64 === undefined
|
|
585
|
+
? utf8(event.data).byteLength
|
|
586
|
+
: processBytes(event.base64).length));
|
|
485
587
|
}
|
|
486
588
|
yield event;
|
|
487
589
|
}
|
|
488
590
|
}
|
|
591
|
+
function processBytes(encoded) {
|
|
592
|
+
try {
|
|
593
|
+
return atob(encoded);
|
|
594
|
+
}
|
|
595
|
+
catch (cause) {
|
|
596
|
+
throw new RuntimeError({
|
|
597
|
+
code: "invalid_process_output",
|
|
598
|
+
status: 502,
|
|
599
|
+
message: "The process returned invalid base64 output.",
|
|
600
|
+
cause,
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
}
|
|
489
604
|
/** A stream cut off by the network rather than refused by Runtime: worth
|
|
490
605
|
* reconnecting to from the cursor. */
|
|
491
606
|
function cutOff(error) {
|
|
@@ -637,6 +752,7 @@ export class Process {
|
|
|
637
752
|
sandboxId;
|
|
638
753
|
info;
|
|
639
754
|
#inputOffset;
|
|
755
|
+
#inputWrites = Promise.resolve();
|
|
640
756
|
constructor(t, sandboxId, info) {
|
|
641
757
|
this.t = t;
|
|
642
758
|
this.sandboxId = sandboxId;
|
|
@@ -650,6 +766,34 @@ export class Process {
|
|
|
650
766
|
output(options = {}) {
|
|
651
767
|
return new Processes(this.t, this.sandboxId).follow(this.id, options);
|
|
652
768
|
}
|
|
769
|
+
/** Lossless output for a process spawned with outputEncoding: "base64".
|
|
770
|
+
* Refuses text-only output rather than inventing bytes that were discarded. */
|
|
771
|
+
async *outputBytes(options = {}) {
|
|
772
|
+
if (this.info.outputEncoding !== "base64")
|
|
773
|
+
throw new RuntimeError({
|
|
774
|
+
code: "binary_output_unavailable",
|
|
775
|
+
status: 409,
|
|
776
|
+
message: "Spawn the process with outputEncoding: base64 to read lossless bytes.",
|
|
777
|
+
});
|
|
778
|
+
for await (const event of this.output(options)) {
|
|
779
|
+
if (event.type === "stdout" || event.type === "stderr") {
|
|
780
|
+
if (event.base64 === undefined)
|
|
781
|
+
throw new RuntimeError({
|
|
782
|
+
code: "binary_output_unavailable",
|
|
783
|
+
status: 502,
|
|
784
|
+
message: "The process returned text without its original bytes.",
|
|
785
|
+
});
|
|
786
|
+
const raw = processBytes(event.base64);
|
|
787
|
+
yield {
|
|
788
|
+
type: event.type,
|
|
789
|
+
offset: event.offset,
|
|
790
|
+
data: Uint8Array.from(raw, (c) => c.charCodeAt(0)),
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
else
|
|
794
|
+
yield event;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
653
797
|
/** Waits for the process to end and returns its result. */
|
|
654
798
|
async wait(options = {}) {
|
|
655
799
|
const out = { stdout: "", stderr: "" };
|
|
@@ -672,22 +816,37 @@ export class Process {
|
|
|
672
816
|
};
|
|
673
817
|
}
|
|
674
818
|
/** Sends input. Offsets are tracked for you, so a retried write is never typed twice. */
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
819
|
+
write(data, options = {}) {
|
|
820
|
+
// Copy before queuing: callers may reuse their buffer while a previous
|
|
821
|
+
// write waits for the guest to drain its pipe.
|
|
822
|
+
const bytes = new Uint8Array(utf8(data));
|
|
823
|
+
const eof = options.eof === true;
|
|
824
|
+
const write = this.#inputWrites.then(async () => {
|
|
825
|
+
let sent = 0;
|
|
826
|
+
do {
|
|
827
|
+
const chunk = bytes.subarray(sent, sent + 1_048_576);
|
|
828
|
+
const reply = await this.t.json({
|
|
829
|
+
method: "POST",
|
|
830
|
+
path: `/v1/sandboxes/${enc(this.sandboxId)}/processes/${enc(this.id)}:write`,
|
|
831
|
+
body: {
|
|
832
|
+
base64: base64(chunk),
|
|
833
|
+
offset: this.#inputOffset,
|
|
834
|
+
...(eof && sent + chunk.length === bytes.length ? { eof: true } : {}),
|
|
835
|
+
},
|
|
836
|
+
});
|
|
837
|
+
const accepted = reply.offset - this.#inputOffset;
|
|
838
|
+
if (!Number.isSafeInteger(reply.offset) || accepted < 0 || accepted > chunk.length)
|
|
839
|
+
throw new ConnectionError({
|
|
840
|
+
message: "The process returned an invalid input offset.",
|
|
841
|
+
code: "connection_error",
|
|
842
|
+
status: 0,
|
|
843
|
+
});
|
|
844
|
+
sent += accepted;
|
|
845
|
+
this.#inputOffset = reply.offset;
|
|
846
|
+
} while (sent < bytes.length);
|
|
847
|
+
});
|
|
848
|
+
this.#inputWrites = write.catch(() => undefined);
|
|
849
|
+
return write;
|
|
691
850
|
}
|
|
692
851
|
async kill(signal = "SIGTERM") {
|
|
693
852
|
await this.t.json({
|
|
@@ -943,44 +1102,64 @@ export class Files {
|
|
|
943
1102
|
}
|
|
944
1103
|
/** Directory entries; `depth` goes deeper, `glob` filters (e.g. "**\/*.py"). */
|
|
945
1104
|
async list(path = "/workspace", options = {}) {
|
|
1105
|
+
const { signal, timeoutMs, idempotencyKey, ...query } = options;
|
|
946
1106
|
return (await this.t.json({
|
|
947
1107
|
method: "GET",
|
|
948
1108
|
path: this.#path("/files/list"),
|
|
949
|
-
query: { path, ...
|
|
1109
|
+
query: { path, ...query },
|
|
1110
|
+
signal,
|
|
1111
|
+
timeoutMs,
|
|
1112
|
+
idempotencyKey,
|
|
950
1113
|
})).data;
|
|
951
1114
|
}
|
|
952
1115
|
async glob(pattern, root = "/workspace") {
|
|
953
1116
|
return this.list(root, { glob: pattern });
|
|
954
1117
|
}
|
|
955
|
-
async stat(path) {
|
|
956
|
-
return this.t.json({
|
|
1118
|
+
async stat(path, options = {}) {
|
|
1119
|
+
return this.t.json({
|
|
1120
|
+
method: "GET",
|
|
1121
|
+
path: this.#path("/files/stat"),
|
|
1122
|
+
query: { path },
|
|
1123
|
+
...options,
|
|
1124
|
+
});
|
|
957
1125
|
}
|
|
958
|
-
async exists(path) {
|
|
959
|
-
return (await this.stat(path)).exists;
|
|
1126
|
+
async exists(path, options = {}) {
|
|
1127
|
+
return (await this.stat(path, options)).exists;
|
|
960
1128
|
}
|
|
961
1129
|
async mkdir(path, options = {}) {
|
|
1130
|
+
const { parents, ...request } = options;
|
|
962
1131
|
await this.t.json({
|
|
963
1132
|
method: "POST",
|
|
964
1133
|
path: this.#path("/files:mkdir"),
|
|
965
|
-
body: { path,
|
|
1134
|
+
body: { path, parents },
|
|
1135
|
+
...request,
|
|
966
1136
|
});
|
|
967
1137
|
}
|
|
968
1138
|
async remove(path, options = {}) {
|
|
1139
|
+
const { recursive, ...request } = options;
|
|
969
1140
|
return (await this.t.json({
|
|
970
1141
|
method: "POST",
|
|
971
1142
|
path: this.#path("/files:remove"),
|
|
972
|
-
body: { path,
|
|
1143
|
+
body: { path, recursive },
|
|
1144
|
+
...request,
|
|
973
1145
|
})).removed;
|
|
974
1146
|
}
|
|
975
1147
|
async rename(from, to, options = {}) {
|
|
1148
|
+
const { overwrite, ...request } = options;
|
|
976
1149
|
await this.t.json({
|
|
977
1150
|
method: "POST",
|
|
978
1151
|
path: this.#path("/files:rename"),
|
|
979
|
-
body: {
|
|
1152
|
+
body: {
|
|
1153
|
+
from,
|
|
1154
|
+
to,
|
|
1155
|
+
...(overwrite === undefined ? {} : { overwrite }),
|
|
1156
|
+
},
|
|
1157
|
+
...request,
|
|
980
1158
|
});
|
|
981
1159
|
}
|
|
982
1160
|
/** Copies a local file or directory into the sandbox. A directory travels as
|
|
983
|
-
* one gzipped tar and
|
|
1161
|
+
* one gzipped tar to the API's folder routes, and the sandbox's own `tar`
|
|
1162
|
+
* unpacks it as it arrives, making the folder. */
|
|
984
1163
|
async upload(localPath, remotePath) {
|
|
985
1164
|
const { stat, readFile } = await import("node:fs/promises");
|
|
986
1165
|
const info = await stat(localPath);
|
|
@@ -1000,20 +1179,39 @@ export class Files {
|
|
|
1000
1179
|
}
|
|
1001
1180
|
const { packDirectory } = await import("./tar.js");
|
|
1002
1181
|
const archive = await packDirectory(localPath);
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1182
|
+
if (archive.length <= CHUNK) {
|
|
1183
|
+
await this.t.bytes({
|
|
1184
|
+
method: "PUT",
|
|
1185
|
+
path: this.#path("/files/archive"),
|
|
1186
|
+
query: { path: remotePath },
|
|
1187
|
+
bytes: archive,
|
|
1188
|
+
});
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
// Larger: parts in order, each unpacked as it arrives; a part resent
|
|
1192
|
+
// after a lost answer is not written twice.
|
|
1193
|
+
const begin = await this.t.json({
|
|
1194
|
+
method: "POST",
|
|
1195
|
+
path: this.#path("/files/archive/uploads"),
|
|
1196
|
+
body: { path: remotePath, gzip: true },
|
|
1197
|
+
});
|
|
1198
|
+
const upload = (suffix = "") => this.#path(`/files/archive/uploads/${enc(begin.uploadId)}${suffix}`);
|
|
1199
|
+
try {
|
|
1200
|
+
for (let offset = 0; offset < archive.length; offset += begin.chunkBytes)
|
|
1201
|
+
await this.t.bytes({
|
|
1202
|
+
method: "PUT",
|
|
1203
|
+
path: upload(),
|
|
1204
|
+
query: { offset },
|
|
1205
|
+
bytes: archive.subarray(offset, offset + begin.chunkBytes),
|
|
1206
|
+
});
|
|
1207
|
+
await this.t.json({ method: "POST", path: upload(":commit"), body: {} });
|
|
1208
|
+
}
|
|
1209
|
+
catch (error) {
|
|
1210
|
+
await this.t
|
|
1211
|
+
.json({ method: "POST", path: upload(":abort"), body: {} })
|
|
1212
|
+
.catch(() => undefined);
|
|
1213
|
+
throw error;
|
|
1214
|
+
}
|
|
1017
1215
|
}
|
|
1018
1216
|
/** Streams a file to disk through a partial file beside the target, renamed
|
|
1019
1217
|
* into place only once every byte has arrived: a failed copy never leaves
|
|
@@ -1048,7 +1246,8 @@ export class Files {
|
|
|
1048
1246
|
}
|
|
1049
1247
|
}
|
|
1050
1248
|
/** Copies a file or directory out of the sandbox. A file streams to disk,
|
|
1051
|
-
* any size, checked against its length
|
|
1249
|
+
* any size, checked against its length; a directory comes as one tar from
|
|
1250
|
+
* the API's folder route. */
|
|
1052
1251
|
async download(remotePath, localPath) {
|
|
1053
1252
|
const fs = await import("node:fs/promises");
|
|
1054
1253
|
const paths = await import("node:path");
|
|
@@ -1064,18 +1263,16 @@ export class Files {
|
|
|
1064
1263
|
await this.#streamTo(remotePath, localPath);
|
|
1065
1264
|
return;
|
|
1066
1265
|
}
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
const
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
await this.remove(staging).catch(() => undefined);
|
|
1078
|
-
}
|
|
1266
|
+
// The sandbox's own `tar` packs it as it streams, and it is unpacked as
|
|
1267
|
+
// it arrives; one cut short is refused and nothing is put in place.
|
|
1268
|
+
const archive = await this.t.fileStream({
|
|
1269
|
+
method: "GET",
|
|
1270
|
+
path: this.#path("/files/archive"),
|
|
1271
|
+
query: { path: remotePath, gzip: true },
|
|
1272
|
+
accept: "application/gzip",
|
|
1273
|
+
});
|
|
1274
|
+
const { unpackStream } = await import("./tar.js");
|
|
1275
|
+
await unpackStream(archive, localPath);
|
|
1079
1276
|
}
|
|
1080
1277
|
}
|
|
1081
1278
|
export function sandboxPage(t, body, query) {
|
package/dist/snapshots.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ import type { RequestOptions, Transport } from "./transport.js";
|
|
|
6
6
|
export type Snapshot = {
|
|
7
7
|
id: string;
|
|
8
8
|
kind: "snapshot";
|
|
9
|
+
/** Absent on older servers means memory. */
|
|
10
|
+
mode?: "memory" | "disk";
|
|
9
11
|
status: string;
|
|
10
12
|
state: "capturing" | "ready" | "failed" | "deleting" | "deleted";
|
|
11
13
|
name: string | null;
|
|
@@ -36,6 +38,8 @@ export type Snapshot = {
|
|
|
36
38
|
[key: string]: unknown;
|
|
37
39
|
};
|
|
38
40
|
export type SnapshotOptions = {
|
|
41
|
+
/** Save files for a fresh boot, or the whole machine (default). */
|
|
42
|
+
mode?: "memory" | "disk";
|
|
39
43
|
name?: string;
|
|
40
44
|
labels?: Record<string, string>;
|
|
41
45
|
retentionDays?: number;
|
|
@@ -54,7 +58,13 @@ export declare class Snapshots {
|
|
|
54
58
|
name?: string;
|
|
55
59
|
state?: "capturing" | "ready" | "failed" | "deleting";
|
|
56
60
|
limit?: number;
|
|
57
|
-
}): Promise<Page<Snapshot>>;
|
|
61
|
+
}, options?: RequestOptions): Promise<Page<Snapshot>>;
|
|
62
|
+
/** Replace supplied labels or name; omitted fields are preserved. Null clears the name. */
|
|
63
|
+
update(id: string, input: {
|
|
64
|
+
name?: string | null;
|
|
65
|
+
labels?: Record<string, string>;
|
|
66
|
+
ifLabels?: Record<string, string>;
|
|
67
|
+
}, options?: RequestOptions): Promise<Snapshot>;
|
|
58
68
|
delete(id: string, options?: RequestOptions): Promise<void>;
|
|
59
69
|
extend(id: string, retentionDays: number, options?: RequestOptions): Promise<Snapshot>;
|
|
60
70
|
}
|
package/dist/snapshots.js
CHANGED
|
@@ -19,7 +19,7 @@ export class Snapshots {
|
|
|
19
19
|
get(id, options = {}) {
|
|
20
20
|
return this.t.json({ method: "GET", path: `/v1/snapshots/${enc(id)}`, ...options });
|
|
21
21
|
}
|
|
22
|
-
async list(filter = {}) {
|
|
22
|
+
async list(filter = {}, options = {}) {
|
|
23
23
|
const query = {
|
|
24
24
|
sandboxId: filter.sandboxId,
|
|
25
25
|
name: filter.name,
|
|
@@ -31,11 +31,21 @@ export class Snapshots {
|
|
|
31
31
|
method: "GET",
|
|
32
32
|
path: "/v1/snapshots",
|
|
33
33
|
query: { ...query, cursor },
|
|
34
|
+
...options,
|
|
34
35
|
});
|
|
35
36
|
return new Page(body.data, body.nextCursor, (next) => fetchPage(next));
|
|
36
37
|
};
|
|
37
38
|
return fetchPage();
|
|
38
39
|
}
|
|
40
|
+
/** Replace supplied labels or name; omitted fields are preserved. Null clears the name. */
|
|
41
|
+
update(id, input, options = {}) {
|
|
42
|
+
return this.t.json({
|
|
43
|
+
method: "POST",
|
|
44
|
+
path: `/v1/snapshots/${enc(id)}:update`,
|
|
45
|
+
body: input,
|
|
46
|
+
...options,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
39
49
|
async delete(id, options = {}) {
|
|
40
50
|
await this.t.json({
|
|
41
51
|
method: "POST",
|