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
package/dist/daytona/process.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { HOME, LONGEST_MS, quote, resolvePath, WHOLE_OUTPUT, } from "./context.js";
|
|
3
|
-
import { DaytonaCommandAlreadyCompletedError, DaytonaConflictError, DaytonaConnectionError, DaytonaNotFoundError, DaytonaProcessExecutionTimeoutError, DaytonaSessionEndedError, guard, NotSupportedError, } from "./errors.js";
|
|
2
|
+
import { HOME, LONGEST_MS, quote, resolvePath, runAs, WHOLE_OUTPUT, } from "./context.js";
|
|
3
|
+
import { DaytonaCommandAlreadyCompletedError, DaytonaConflictError, DaytonaConnectionError, DaytonaError, DaytonaNotFoundError, DaytonaProcessExecutionTimeoutError, DaytonaSessionEndedError, guard, NotSupportedError, } from "./errors.js";
|
|
4
4
|
export class CodeRunParams {
|
|
5
5
|
argv;
|
|
6
6
|
env;
|
|
@@ -57,6 +57,8 @@ class ShellSession {
|
|
|
57
57
|
export class Process {
|
|
58
58
|
#ctx;
|
|
59
59
|
#sessions = new Map();
|
|
60
|
+
/** What only this client knows of its PTY sessions: envs and size. */
|
|
61
|
+
#ptys = new Map();
|
|
60
62
|
constructor(ctx) {
|
|
61
63
|
this.#ctx = ctx;
|
|
62
64
|
}
|
|
@@ -64,6 +66,15 @@ export class Process {
|
|
|
64
66
|
const merged = { ...this.#ctx.env, ...extra };
|
|
65
67
|
return Object.keys(merged).length ? { env: merged } : {};
|
|
66
68
|
}
|
|
69
|
+
/** A command and its environment option as Runtime runs them: as given
|
|
70
|
+
* for the sandbox owner, through sudo for create's `user`. */
|
|
71
|
+
#as(command, extra) {
|
|
72
|
+
const user = this.#ctx.user;
|
|
73
|
+
if (!user)
|
|
74
|
+
return [command, this.#env(extra)];
|
|
75
|
+
const argv = typeof command === "string" ? ["bash", "-c", command] : command;
|
|
76
|
+
return [runAs(user, argv, { ...this.#ctx.env, ...extra }).argv, {}];
|
|
77
|
+
}
|
|
67
78
|
/** Runs a shell command and resolves with its exit code and output. A
|
|
68
79
|
* non-zero exit is a result, not an error. `timeout` is in seconds; past
|
|
69
80
|
* it the command is killed and DaytonaProcessExecutionTimeoutError is
|
|
@@ -72,9 +83,10 @@ export class Process {
|
|
|
72
83
|
await this.#ctx.ensureHome(`${command}\n${cwd ?? ""}`);
|
|
73
84
|
const runtime = await this.#ctx.live();
|
|
74
85
|
const timeoutMs = timeout ? timeout * 1000 : LONGEST_MS;
|
|
75
|
-
const
|
|
86
|
+
const [run, environment] = this.#as(`{ ${command}\n} 2>&1`, env);
|
|
87
|
+
const result = await guard("sandbox", () => runtime.exec(run, {
|
|
76
88
|
cwd: cwd === undefined ? HOME : resolvePath(cwd),
|
|
77
|
-
...
|
|
89
|
+
...environment,
|
|
78
90
|
timeoutMs,
|
|
79
91
|
...WHOLE_OUTPUT,
|
|
80
92
|
}));
|
|
@@ -96,9 +108,10 @@ export class Process {
|
|
|
96
108
|
? ["bun", "-e", code]
|
|
97
109
|
: ["node", "-e", code];
|
|
98
110
|
const timeoutMs = timeout ? timeout * 1000 : LONGEST_MS;
|
|
99
|
-
const
|
|
111
|
+
const [run, environment] = this.#as(["sh", "-c", 'exec "$@" 2>&1', "sh", ...interpreter, ...(params.argv ?? [])], params.env);
|
|
112
|
+
const result = await guard("sandbox", () => runtime.exec(run, {
|
|
100
113
|
cwd: HOME,
|
|
101
|
-
...
|
|
114
|
+
...environment,
|
|
102
115
|
timeoutMs,
|
|
103
116
|
...WHOLE_OUTPUT,
|
|
104
117
|
}));
|
|
@@ -120,9 +133,16 @@ export class Process {
|
|
|
120
133
|
if (this.#sessions.has(sessionId) || (await this.#discover(sessionId)))
|
|
121
134
|
throw new DaytonaConflictError(`Session ${sessionId} already exists.`, 409);
|
|
122
135
|
const runtime = await this.#ctx.live();
|
|
123
|
-
const
|
|
136
|
+
const [run, environment] = this.#as([
|
|
137
|
+
"bash",
|
|
138
|
+
"--noprofile",
|
|
139
|
+
"--norc",
|
|
140
|
+
"-s",
|
|
141
|
+
`${TAG}${sessionId}`,
|
|
142
|
+
]);
|
|
143
|
+
const process = await guard("sandbox", () => runtime.spawn(run, {
|
|
124
144
|
cwd: HOME,
|
|
125
|
-
...
|
|
145
|
+
...environment,
|
|
126
146
|
stdin: "pipe",
|
|
127
147
|
timeoutMs: LONGEST_MS,
|
|
128
148
|
}));
|
|
@@ -329,25 +349,209 @@ export class Process {
|
|
|
329
349
|
return entrypoint();
|
|
330
350
|
}
|
|
331
351
|
// ---- terminals ---------------------------------------------------------
|
|
332
|
-
|
|
333
|
-
|
|
352
|
+
/** Starts an interactive shell on a terminal (a PTY) that outlives this
|
|
353
|
+
* connection: disconnect() leaves it running, connectPty(id) attaches
|
|
354
|
+
* again, killPtySession(id) ends it. On Runtime it is a process started
|
|
355
|
+
* with a terminal, found again by its id among the sandbox's processes. */
|
|
356
|
+
async createPty(options) {
|
|
357
|
+
const id = options?.id;
|
|
358
|
+
if (!id)
|
|
359
|
+
throw new DaytonaError("A PTY session needs an id.", 400);
|
|
360
|
+
if (await this.#findPty(id))
|
|
361
|
+
throw new DaytonaConflictError(`PTY session ${id} already exists.`, 409);
|
|
362
|
+
const cols = options.cols ?? 80;
|
|
363
|
+
const rows = options.rows ?? 24;
|
|
364
|
+
await this.#ctx.ensureHome(options.cwd);
|
|
365
|
+
const cwd = options.cwd === undefined ? HOME : resolvePath(options.cwd);
|
|
366
|
+
const envs = { ...options.envs };
|
|
367
|
+
const runtime = await this.#ctx.live();
|
|
368
|
+
const [run, environment] = this.#as(["bash", "-l", "-i", "-s", ptyTag(id), `${cols}x${rows}`], {
|
|
369
|
+
TERM: "xterm-256color",
|
|
370
|
+
...envs,
|
|
371
|
+
});
|
|
372
|
+
const process = await guard("sandbox", () => runtime.spawn(run, { cwd, ...environment, pty: { cols, rows }, timeoutMs: LONGEST_MS }));
|
|
373
|
+
this.#ptys.set(id, { envs, cols, rows });
|
|
374
|
+
try {
|
|
375
|
+
return await this.#attach(id, process.id, options.onData);
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
await process.kill("SIGKILL").catch(() => undefined);
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
/** Attaches to a running PTY session. Output it printed before arrives
|
|
383
|
+
* first, then what it prints from now on. */
|
|
384
|
+
async connectPty(sessionId, options) {
|
|
385
|
+
const found = await this.#findPty(sessionId);
|
|
386
|
+
if (!found)
|
|
387
|
+
throw new DaytonaNotFoundError(`PTY session ${sessionId} was not found.`, 404);
|
|
388
|
+
return this.#attach(sessionId, found.id, options?.onData);
|
|
389
|
+
}
|
|
390
|
+
async listPtySessions() {
|
|
391
|
+
const latest = new Map();
|
|
392
|
+
for (const info of await this.#ptyProcesses())
|
|
393
|
+
latest.set(ptyOf(info.command).id, info);
|
|
394
|
+
return [...latest.values()].map((info) => this.#ptyInfo(info));
|
|
395
|
+
}
|
|
396
|
+
async getPtySessionInfo(sessionId) {
|
|
397
|
+
const found = (await this.#ptyProcesses())
|
|
398
|
+
.filter((info) => ptyOf(info.command).id === sessionId)
|
|
399
|
+
.at(-1);
|
|
400
|
+
if (!found)
|
|
401
|
+
throw new DaytonaNotFoundError(`PTY session ${sessionId} was not found.`, 404);
|
|
402
|
+
return this.#ptyInfo(found);
|
|
403
|
+
}
|
|
404
|
+
/** Ends the session's shell and everything it runs. */
|
|
405
|
+
async killPtySession(sessionId) {
|
|
406
|
+
const process = await this.#ptyProcess(sessionId);
|
|
407
|
+
await guard("process", () => process.kill("SIGKILL"));
|
|
408
|
+
}
|
|
409
|
+
async resizePtySession(sessionId, cols, rows) {
|
|
410
|
+
const process = await this.#ptyProcess(sessionId);
|
|
411
|
+
await guard("process", () => process.resize(cols, rows));
|
|
412
|
+
const known = this.#ptys.get(sessionId);
|
|
413
|
+
this.#ptys.set(sessionId, { envs: known?.envs ?? {}, cols, rows });
|
|
414
|
+
return this.#ptyInfo({ ...process.info, state: "running" });
|
|
415
|
+
}
|
|
416
|
+
/** Terminal processes this adapter started, a killed one gone as on
|
|
417
|
+
* Daytona; an exited one stays, inactive. */
|
|
418
|
+
async #ptyProcesses() {
|
|
419
|
+
const runtime = await this.#ctx.live();
|
|
420
|
+
const list = await guard("sandbox", () => runtime.processes.list());
|
|
421
|
+
return list.filter((info) => info.state !== "killed" && ptyOf(info.command));
|
|
422
|
+
}
|
|
423
|
+
async #findPty(sessionId) {
|
|
424
|
+
return (await this.#ptyProcesses()).find((info) => info.state === "running" && ptyOf(info.command).id === sessionId);
|
|
425
|
+
}
|
|
426
|
+
async #ptyProcess(sessionId) {
|
|
427
|
+
const found = await this.#findPty(sessionId);
|
|
428
|
+
if (!found)
|
|
429
|
+
throw new DaytonaNotFoundError(`PTY session ${sessionId} was not found.`, 404);
|
|
430
|
+
const runtime = await this.#ctx.live();
|
|
431
|
+
return guard("process", () => runtime.processes.get(found.id));
|
|
432
|
+
}
|
|
433
|
+
#ptyInfo(info) {
|
|
434
|
+
const tag = ptyOf(info.command);
|
|
435
|
+
const known = this.#ptys.get(tag.id);
|
|
436
|
+
return {
|
|
437
|
+
active: info.state === "running",
|
|
438
|
+
cols: known?.cols ?? tag.cols,
|
|
439
|
+
createdAt: info.startedAt,
|
|
440
|
+
cwd: info.cwd,
|
|
441
|
+
envs: { ...known?.envs },
|
|
442
|
+
id: tag.id,
|
|
443
|
+
lazyStart: false,
|
|
444
|
+
rows: known?.rows ?? tag.rows,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
async #attach(sessionId, processId, onData) {
|
|
448
|
+
const runtime = await this.#ctx.live();
|
|
449
|
+
const terminal = await guard("sandbox", () => runtime.terminal({
|
|
450
|
+
processId,
|
|
451
|
+
...(onData
|
|
452
|
+
? {
|
|
453
|
+
onData: (data) => void Promise.resolve()
|
|
454
|
+
.then(() => onData(data))
|
|
455
|
+
.catch(() => undefined),
|
|
456
|
+
}
|
|
457
|
+
: {}),
|
|
458
|
+
}));
|
|
459
|
+
return new PtyHandle(terminal, sessionId, (cols, rows) => this.resizePtySession(sessionId, cols, rows), () => this.killPtySession(sessionId));
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
/** One connection to a PTY session, Daytona's PtyHandle over Runtime's
|
|
463
|
+
* terminal WebSocket. */
|
|
464
|
+
export class PtyHandle {
|
|
465
|
+
sessionId;
|
|
466
|
+
#terminal;
|
|
467
|
+
#resize;
|
|
468
|
+
#kill;
|
|
469
|
+
#result;
|
|
470
|
+
#connected = true;
|
|
471
|
+
#disconnected = false;
|
|
472
|
+
#exitCode;
|
|
473
|
+
#error;
|
|
474
|
+
/** Use process.createPty or process.connectPty. */
|
|
475
|
+
constructor(terminal, sessionId, resize, kill) {
|
|
476
|
+
this.sessionId = sessionId;
|
|
477
|
+
this.#terminal = terminal;
|
|
478
|
+
this.#resize = resize;
|
|
479
|
+
this.#kill = kill;
|
|
480
|
+
this.#result = terminal.exited.then((code) => {
|
|
481
|
+
this.#connected = false;
|
|
482
|
+
if (code !== null)
|
|
483
|
+
this.#exitCode = code;
|
|
484
|
+
else
|
|
485
|
+
this.#error = this.#disconnected
|
|
486
|
+
? "Disconnected; the PTY session keeps running. connectPty() attaches again."
|
|
487
|
+
: "The PTY session was ended by a signal.";
|
|
488
|
+
return {
|
|
489
|
+
...(this.#exitCode === undefined ? {} : { exitCode: this.#exitCode }),
|
|
490
|
+
...(this.#error === undefined ? {} : { error: this.#error }),
|
|
491
|
+
};
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
get exitCode() {
|
|
495
|
+
return this.#exitCode;
|
|
496
|
+
}
|
|
497
|
+
get error() {
|
|
498
|
+
return this.#error;
|
|
334
499
|
}
|
|
335
|
-
|
|
336
|
-
return
|
|
500
|
+
isConnected() {
|
|
501
|
+
return this.#connected;
|
|
337
502
|
}
|
|
338
|
-
|
|
339
|
-
|
|
503
|
+
/** Resolves at once: the handle is returned connected. */
|
|
504
|
+
async waitForConnection() {
|
|
505
|
+
if (!this.#connected && this.#exitCode === undefined)
|
|
506
|
+
throw new DaytonaConnectionError(this.#error ?? "Connection closed");
|
|
340
507
|
}
|
|
341
|
-
|
|
342
|
-
|
|
508
|
+
async sendInput(data) {
|
|
509
|
+
if (!this.#connected)
|
|
510
|
+
throw new DaytonaConnectionError("PTY is not connected");
|
|
511
|
+
try {
|
|
512
|
+
this.#terminal.write(data);
|
|
513
|
+
}
|
|
514
|
+
catch (error) {
|
|
515
|
+
throw new DaytonaConnectionError(`Failed to send input to PTY: ${error instanceof Error ? error.message : String(error)}`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
resize(cols, rows) {
|
|
519
|
+
return this.#resize(cols, rows);
|
|
520
|
+
}
|
|
521
|
+
/** Closes this connection; the session keeps running. */
|
|
522
|
+
async disconnect() {
|
|
523
|
+
if (!this.#connected)
|
|
524
|
+
return;
|
|
525
|
+
this.#disconnected = true;
|
|
526
|
+
this.#terminal.close();
|
|
343
527
|
}
|
|
344
|
-
|
|
345
|
-
|
|
528
|
+
/** Resolves when the session ends (or this connection closes) with its
|
|
529
|
+
* exit code, or `error` saying why there is none. */
|
|
530
|
+
wait() {
|
|
531
|
+
return this.#result;
|
|
346
532
|
}
|
|
347
|
-
|
|
348
|
-
return
|
|
533
|
+
kill() {
|
|
534
|
+
return this.#kill();
|
|
349
535
|
}
|
|
350
536
|
}
|
|
537
|
+
const PTY_TAG = "daytona-pty:";
|
|
538
|
+
/** A session id as one word of the shell's command line, found again by
|
|
539
|
+
* listPtySessions in any client. */
|
|
540
|
+
function ptyTag(id) {
|
|
541
|
+
return PTY_TAG + Buffer.from(id).toString("base64url");
|
|
542
|
+
}
|
|
543
|
+
function ptyOf(command) {
|
|
544
|
+
const words = String(command).split(" ");
|
|
545
|
+
const at = words.findIndex((word) => word.startsWith(PTY_TAG));
|
|
546
|
+
if (at < 0)
|
|
547
|
+
return undefined;
|
|
548
|
+
const size = /^(\d+)x(\d+)$/.exec(words[at + 1] ?? "");
|
|
549
|
+
return {
|
|
550
|
+
id: Buffer.from(words[at].slice(PTY_TAG.length), "base64url").toString(),
|
|
551
|
+
cols: Number(size?.[1] ?? 80),
|
|
552
|
+
rows: Number(size?.[2] ?? 24),
|
|
553
|
+
};
|
|
554
|
+
}
|
|
351
555
|
function describe(command) {
|
|
352
556
|
return {
|
|
353
557
|
id: command.id,
|
|
@@ -355,9 +559,6 @@ function describe(command) {
|
|
|
355
559
|
...(command.exitCode === undefined ? {} : { exitCode: command.exitCode }),
|
|
356
560
|
};
|
|
357
561
|
}
|
|
358
|
-
function pty() {
|
|
359
|
-
return Promise.reject(new NotSupportedError("Daytona's PTY sessions", "Use `await sandbox.withruntime.terminal({ cols, rows, onData })` for an interactive terminal, or a session for commands."));
|
|
360
|
-
}
|
|
361
562
|
function entrypoint() {
|
|
362
563
|
return Promise.reject(new NotSupportedError("The image entrypoint session", "A Runtime image does not run an entrypoint; start it with process.createSession and executeSessionCommand."));
|
|
363
564
|
}
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import type { Runtime } from "../client.js";
|
|
2
2
|
import type { RuntimeSandbox } from "./client.js";
|
|
3
|
+
import { ComputerUse } from "./computer-use.js";
|
|
3
4
|
import { DaytonaNotFoundError } from "./errors.js";
|
|
4
5
|
import { FileSystem } from "./filesystem.js";
|
|
5
6
|
import { Git } from "./git.js";
|
|
6
7
|
import { Process } from "./process.js";
|
|
7
8
|
/** How the sandbox's lease follows Daytona's lifecycle settings. */
|
|
8
9
|
export interface Lifecycle {
|
|
9
|
-
/**
|
|
10
|
-
*
|
|
10
|
+
/** The lease each call renews, 60 to 3600 seconds: Runtime's longest
|
|
11
|
+
* lease is an hour. */
|
|
11
12
|
windowSeconds: number;
|
|
13
|
+
/** Seconds without a call from this client before the sandbox pauses
|
|
14
|
+
* (Daytona's autoStopInterval); Infinity for 0, never. Past an hour the
|
|
15
|
+
* lease is renewed an hour at a time while this object lives. When
|
|
16
|
+
* absent, windowSeconds. */
|
|
17
|
+
idleSeconds?: number;
|
|
12
18
|
/** Epoch ms past which the lease is never extended (ttlMinutes). */
|
|
13
19
|
deadline?: number;
|
|
14
20
|
/** stop() ends the sandbox instead of pausing it (ephemeral, or
|
|
@@ -24,6 +30,8 @@ export interface SandboxOptions {
|
|
|
24
30
|
public: boolean;
|
|
25
31
|
lifecycle: Lifecycle;
|
|
26
32
|
snapshot?: string;
|
|
33
|
+
/** create's `user`, when not the sandbox owner. */
|
|
34
|
+
user?: string;
|
|
27
35
|
}
|
|
28
36
|
export type SandboxState = "started" | "starting" | "stopping" | "stopped" | "paused" | "destroying" | "destroyed";
|
|
29
37
|
export interface PortPreviewUrl {
|
|
@@ -90,7 +98,8 @@ export declare class Sandbox {
|
|
|
90
98
|
get name(): string;
|
|
91
99
|
get organizationId(): string;
|
|
92
100
|
/** Daytona's user name, so `/home/${sandbox.user}` leads to the working
|
|
93
|
-
* directory
|
|
101
|
+
* directory, and commands run as Runtime's sandbox owner, with passwordless
|
|
102
|
+
* sudo; or the user given at create, whose commands run through sudo. */
|
|
94
103
|
get user(): string;
|
|
95
104
|
get labels(): Record<string, string>;
|
|
96
105
|
get target(): string;
|
|
@@ -125,8 +134,9 @@ export declare class Sandbox {
|
|
|
125
134
|
refreshData(): Promise<void>;
|
|
126
135
|
/** Counts as activity: moves the lease a full autoStop window on. */
|
|
127
136
|
refreshActivity(): Promise<void>;
|
|
128
|
-
/** The sandbox pauses after `interval` minutes
|
|
129
|
-
*
|
|
137
|
+
/** The sandbox pauses after `interval` minutes with nothing happening in
|
|
138
|
+
* it; 0 never. With a time to live, after that long without a call from
|
|
139
|
+
* this client, and past an hour only while this object lives. */
|
|
130
140
|
setAutostopInterval(interval: number): Promise<void>;
|
|
131
141
|
setAutoPauseInterval(interval: number): Promise<void>;
|
|
132
142
|
/** Runtime has no archive tier; recorded only. */
|
|
@@ -161,7 +171,9 @@ export declare class Sandbox {
|
|
|
161
171
|
* named `name`; `daytona.create({ snapshot: name })` starts from it. */
|
|
162
172
|
createSnapshot(name: string, _timeout?: number): Promise<void>;
|
|
163
173
|
_experimental_createSnapshot(name: string, timeout?: number): Promise<void>;
|
|
164
|
-
|
|
174
|
+
/** Daytona's computer use over Runtime's desktop: `await
|
|
175
|
+
* sandbox.computerUse.start()`, then the mouse, keyboard and screenshots. */
|
|
176
|
+
get computerUse(): ComputerUse;
|
|
165
177
|
setLabels(labels: Record<string, string>): Promise<Record<string, string>>;
|
|
166
178
|
recover(): Promise<never>;
|
|
167
179
|
resize(): Promise<never>;
|
|
@@ -181,6 +193,13 @@ export declare class Sandbox {
|
|
|
181
193
|
}
|
|
182
194
|
/** Daytona's autoStopInterval (minutes; 0 is off) as the lease window. */
|
|
183
195
|
export declare function windowSeconds(autoStopMinutes: number): number;
|
|
196
|
+
/** Daytona's autoStopInterval (minutes; 0 is never) as the idle pause of a
|
|
197
|
+
* sandbox with no time limit: seconds with nothing happening in it, counted
|
|
198
|
+
* by the sandbox itself (`idlePauseSeconds`, 0 never, at most a day). */
|
|
199
|
+
export declare function idlePauseOf(autoStopMinutes: number): number;
|
|
200
|
+
/** Daytona's autoStopInterval (minutes; 0 is never) as seconds without a
|
|
201
|
+
* call before a sandbox with a time to live pauses. */
|
|
202
|
+
export declare function idleSeconds(autoStopMinutes: number): number;
|
|
184
203
|
/** Minutes as Runtime's retention: whole days, 1 to 365. */
|
|
185
204
|
export declare function retentionDays(minutes: number): number;
|
|
186
205
|
/** Daytona's network settings as Runtime's rules, or undefined for the
|
package/dist/daytona/sandbox.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { LeaseKeeper } from "../compat/lease.js";
|
|
2
|
+
import { ComputerUse } from "./computer-use.js";
|
|
2
3
|
import { HOME, HOME_LINK } from "./context.js";
|
|
3
4
|
import { DaytonaError, DaytonaNotFoundError, guard, NotSupportedError } from "./errors.js";
|
|
4
5
|
import { FileSystem } from "./filesystem.js";
|
|
@@ -96,6 +97,10 @@ export class Sandbox {
|
|
|
96
97
|
#options;
|
|
97
98
|
#home;
|
|
98
99
|
#pausedByPause = false;
|
|
100
|
+
#computerUse;
|
|
101
|
+
/** When this client last made a call: autoStopInterval counts from it. */
|
|
102
|
+
#lastActive = Date.now();
|
|
103
|
+
#keeper;
|
|
99
104
|
/** Use daytona.create or daytona.get. */
|
|
100
105
|
constructor(runtime, client, options) {
|
|
101
106
|
this.#rt = runtime;
|
|
@@ -104,9 +109,20 @@ export class Sandbox {
|
|
|
104
109
|
this.env = { ...options.env };
|
|
105
110
|
this.public = options.public;
|
|
106
111
|
this.snapshot = options.snapshot;
|
|
112
|
+
this.#keeper = new LeaseKeeper({
|
|
113
|
+
sandbox: () => this.#rt,
|
|
114
|
+
until: () => {
|
|
115
|
+
const lifecycle = this.#options.lifecycle;
|
|
116
|
+
return Math.min(this.#lastActive + (lifecycle.idleSeconds ?? lifecycle.windowSeconds) * 1000, lifecycle.deadline ?? Infinity);
|
|
117
|
+
},
|
|
118
|
+
// Renewed when less than half the window is left, so a run of calls
|
|
119
|
+
// costs one extend per half window.
|
|
120
|
+
marginMs: () => this.#options.lifecycle.windowSeconds * 500,
|
|
121
|
+
});
|
|
107
122
|
const ctx = {
|
|
108
123
|
live: () => this.#live(),
|
|
109
124
|
language: options.language,
|
|
125
|
+
...(options.user ? { user: options.user } : {}),
|
|
110
126
|
ensureHome: (text) => this.#ensureHome(text),
|
|
111
127
|
};
|
|
112
128
|
// A getter, so the modules see the environment updateEnv sets.
|
|
@@ -131,9 +147,10 @@ export class Sandbox {
|
|
|
131
147
|
return "";
|
|
132
148
|
}
|
|
133
149
|
/** Daytona's user name, so `/home/${sandbox.user}` leads to the working
|
|
134
|
-
* directory
|
|
150
|
+
* directory, and commands run as Runtime's sandbox owner, with passwordless
|
|
151
|
+
* sudo; or the user given at create, whose commands run through sudo. */
|
|
135
152
|
get user() {
|
|
136
|
-
return "daytona";
|
|
153
|
+
return this.#options.user ?? "daytona";
|
|
137
154
|
}
|
|
138
155
|
get labels() {
|
|
139
156
|
return { ...this.#rt.info.labels };
|
|
@@ -195,29 +212,13 @@ export class Sandbox {
|
|
|
195
212
|
}
|
|
196
213
|
/** The sandbox, its lease moved on when less than half of the autoStop
|
|
197
214
|
* window is left: every call through this object counts as activity, as
|
|
198
|
-
* Daytona's API calls do.
|
|
215
|
+
* Daytona's API calls do. An autoStopInterval past an hour, or 0, is kept
|
|
216
|
+
* by renewing the lease while this object lives. */
|
|
199
217
|
async #live(force = false) {
|
|
200
|
-
const lifecycle = this.#options.lifecycle;
|
|
201
218
|
if (this.#rt.state !== "running")
|
|
202
219
|
return this.#rt;
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
return this.#rt;
|
|
206
|
-
let seconds = Math.ceil(lifecycle.windowSeconds - left / 1000);
|
|
207
|
-
if (lifecycle.deadline !== undefined)
|
|
208
|
-
seconds = Math.min(seconds, Math.floor((lifecycle.deadline - Date.now() - left) / 1000));
|
|
209
|
-
if (seconds < 1)
|
|
210
|
-
return this.#rt;
|
|
211
|
-
try {
|
|
212
|
-
await this.#rt.extend(seconds);
|
|
213
|
-
}
|
|
214
|
-
catch (error) {
|
|
215
|
-
// The lease may have ended since this object last looked; the call
|
|
216
|
-
// itself then says what state the sandbox is in.
|
|
217
|
-
if (!(error instanceof RuntimeError && error.status === 409))
|
|
218
|
-
throw error;
|
|
219
|
-
await this.#rt.refresh().catch(() => undefined);
|
|
220
|
-
}
|
|
220
|
+
this.#lastActive = Date.now();
|
|
221
|
+
await this.#keeper.check(force);
|
|
221
222
|
return this.#rt;
|
|
222
223
|
}
|
|
223
224
|
#ensureHome(text) {
|
|
@@ -237,7 +238,11 @@ export class Sandbox {
|
|
|
237
238
|
if (state === "stopped" || state === "stopping")
|
|
238
239
|
throw new DaytonaError(`Sandbox ${this.id} was deleted and cannot start.`, 410);
|
|
239
240
|
if (state === "paused" || state === "pausing")
|
|
240
|
-
await guard("sandbox", () =>
|
|
241
|
+
await guard("sandbox", () =>
|
|
242
|
+
// One with no time limit (timeoutSeconds 0) wakes with none.
|
|
243
|
+
this.#rt.wake(this.#rt.info.timeoutSeconds
|
|
244
|
+
? { timeoutSeconds: this.#options.lifecycle.windowSeconds }
|
|
245
|
+
: {}));
|
|
241
246
|
this.#pausedByPause = false;
|
|
242
247
|
}
|
|
243
248
|
/** Stops the sandbox: Runtime pauses it, keeping its files and memory, so
|
|
@@ -248,6 +253,7 @@ export class Sandbox {
|
|
|
248
253
|
if (state === "stopped" || state === "stopping")
|
|
249
254
|
return;
|
|
250
255
|
if (this.#options.lifecycle.ephemeral) {
|
|
256
|
+
this.#keeper.end();
|
|
251
257
|
await guard("sandbox", () => this.#rt.stop());
|
|
252
258
|
return;
|
|
253
259
|
}
|
|
@@ -269,6 +275,7 @@ export class Sandbox {
|
|
|
269
275
|
}
|
|
270
276
|
/** Ends the sandbox for good. */
|
|
271
277
|
async delete(_timeout) {
|
|
278
|
+
this.#keeper.end();
|
|
272
279
|
await guard("sandbox", () => this.#rt.refresh());
|
|
273
280
|
if (this.#rt.state !== "stopped")
|
|
274
281
|
await guard("sandbox", () => this.#rt.stop({ wait: false }));
|
|
@@ -288,12 +295,18 @@ export class Sandbox {
|
|
|
288
295
|
async refreshActivity() {
|
|
289
296
|
await guard("sandbox", () => this.#live(true));
|
|
290
297
|
}
|
|
291
|
-
/** The sandbox pauses after `interval` minutes
|
|
292
|
-
*
|
|
298
|
+
/** The sandbox pauses after `interval` minutes with nothing happening in
|
|
299
|
+
* it; 0 never. With a time to live, after that long without a call from
|
|
300
|
+
* this client, and past an hour only while this object lives. */
|
|
293
301
|
async setAutostopInterval(interval) {
|
|
294
302
|
const lifecycle = this.#options.lifecycle;
|
|
295
303
|
lifecycle.autoStopInterval = interval;
|
|
304
|
+
if (!this.#rt.info.timeoutSeconds) {
|
|
305
|
+
await guard("sandbox", () => this.#rt.update({ idlePauseSeconds: idlePauseOf(interval) }));
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
296
308
|
lifecycle.windowSeconds = windowSeconds(interval);
|
|
309
|
+
lifecycle.idleSeconds = idleSeconds(interval);
|
|
297
310
|
await guard("sandbox", () => this.#live());
|
|
298
311
|
}
|
|
299
312
|
setAutoPauseInterval(interval) {
|
|
@@ -317,6 +330,8 @@ export class Sandbox {
|
|
|
317
330
|
delete this.#options.lifecycle.deadline;
|
|
318
331
|
return;
|
|
319
332
|
}
|
|
333
|
+
if (!this.#rt.info.timeoutSeconds)
|
|
334
|
+
throw new NotSupportedError("A time to live on a sandbox made without one", "It has no time limit and pauses when idle; pass ttlMinutes to create(), or call stop() or delete() when the work is done.");
|
|
320
335
|
const deadline = Date.parse(this.#rt.info.createdAt) + ttlMinutes * 60_000;
|
|
321
336
|
if (Date.parse(this.#rt.info.expiresAt) > deadline)
|
|
322
337
|
throw new NotSupportedError("A time to live shorter than the current lease", "Runtime leases only move later; call stop() or delete() when the work is done.");
|
|
@@ -377,8 +392,11 @@ export class Sandbox {
|
|
|
377
392
|
return this.createSnapshot(name, timeout);
|
|
378
393
|
}
|
|
379
394
|
// ---- what Runtime does differently -----------------------------------------
|
|
395
|
+
/** Daytona's computer use over Runtime's desktop: `await
|
|
396
|
+
* sandbox.computerUse.start()`, then the mouse, keyboard and screenshots. */
|
|
380
397
|
get computerUse() {
|
|
381
|
-
|
|
398
|
+
this.#computerUse ??= new ComputerUse(() => this.#live());
|
|
399
|
+
return this.#computerUse;
|
|
382
400
|
}
|
|
383
401
|
async setLabels(labels) {
|
|
384
402
|
const internal = Object.fromEntries(Object.entries(this.#rt.info.labels).filter(([key]) => key === "code-toolbox-language" || key.startsWith("compat.")));
|
|
@@ -437,6 +455,21 @@ export function windowSeconds(autoStopMinutes) {
|
|
|
437
455
|
return 3600;
|
|
438
456
|
return Math.min(3600, Math.max(60, Math.round(autoStopMinutes * 60)));
|
|
439
457
|
}
|
|
458
|
+
/** Daytona's autoStopInterval (minutes; 0 is never) as the idle pause of a
|
|
459
|
+
* sandbox with no time limit: seconds with nothing happening in it, counted
|
|
460
|
+
* by the sandbox itself (`idlePauseSeconds`, 0 never, at most a day). */
|
|
461
|
+
export function idlePauseOf(autoStopMinutes) {
|
|
462
|
+
if (autoStopMinutes <= 0)
|
|
463
|
+
return 0;
|
|
464
|
+
return Math.min(86_400, Math.max(60, Math.round(autoStopMinutes * 60)));
|
|
465
|
+
}
|
|
466
|
+
/** Daytona's autoStopInterval (minutes; 0 is never) as seconds without a
|
|
467
|
+
* call before a sandbox with a time to live pauses. */
|
|
468
|
+
export function idleSeconds(autoStopMinutes) {
|
|
469
|
+
if (autoStopMinutes <= 0)
|
|
470
|
+
return Infinity;
|
|
471
|
+
return Math.max(60, Math.round(autoStopMinutes * 60));
|
|
472
|
+
}
|
|
440
473
|
/** Minutes as Runtime's retention: whole days, 1 to 365. */
|
|
441
474
|
export function retentionDays(minutes) {
|
|
442
475
|
return Math.min(365, Math.max(1, Math.ceil(minutes / 1440)));
|
package/dist/e2b/client.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { Runtime } from "../client.js";
|
|
|
3
3
|
export type RuntimeSandbox = Awaited<ReturnType<Runtime["sandboxes"]["create"]>>;
|
|
4
4
|
/** What `Sandbox.create` sends to Runtime. */
|
|
5
5
|
export type RuntimeCreate = NonNullable<Parameters<Runtime["sandboxes"]["create"]>[0]>;
|
|
6
|
+
/** E2B's HTTP version for its API and sandboxes (e2b 2.52.0). */
|
|
7
|
+
export type HttpVersion = "1.1" | "2";
|
|
6
8
|
/** E2B's connection options, as far as they mean anything on Runtime. */
|
|
7
9
|
export interface ConnectionOpts {
|
|
8
10
|
/** A Runtime key. Default: RUNTIME_API_KEY, then E2B_API_KEY if it holds a
|
|
@@ -18,6 +20,9 @@ export interface ConnectionOpts {
|
|
|
18
20
|
retries?: number;
|
|
19
21
|
/** Accepted and ignored: Runtime's SDK does not take a logger. */
|
|
20
22
|
logger?: unknown;
|
|
23
|
+
/** Checked as E2B checks it (and E2B_HTTP_VERSION), then left to Runtime's
|
|
24
|
+
* SDK, which picks its own transport. */
|
|
25
|
+
httpVersion?: HttpVersion;
|
|
21
26
|
/** E2B endpoints. Refused: this package only talks to Runtime. */
|
|
22
27
|
domain?: string;
|
|
23
28
|
apiUrl?: string;
|
package/dist/e2b/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Runtime } from "../client.js";
|
|
2
|
-
import { AuthenticationError, NotSupportedError } from "./errors.js";
|
|
2
|
+
import { AuthenticationError, InvalidArgumentError, NotSupportedError } from "./errors.js";
|
|
3
3
|
const env = (name) => typeof process === "undefined" ? undefined : process.env[name];
|
|
4
4
|
const isE2BKey = (key) => key.startsWith("e2b_");
|
|
5
5
|
/** Refuses the E2B-only connection options: they point at E2B, and quietly
|
|
@@ -18,6 +18,10 @@ export function refuseE2BConnection(opts = {}) {
|
|
|
18
18
|
? "Set HTTPS_PROXY in the environment instead; Runtime's SDK reads it."
|
|
19
19
|
: "Remove it: this package talks only to Runtime (RUNTIME_API_URL overrides the API origin).");
|
|
20
20
|
}
|
|
21
|
+
// E2B refuses any other version, from the option or the environment.
|
|
22
|
+
const version = opts.httpVersion ?? (env("E2B_HTTP_VERSION") || undefined);
|
|
23
|
+
if (version !== undefined && version !== "1.1" && version !== "2")
|
|
24
|
+
throw new InvalidArgumentError(`${opts.httpVersion === undefined ? "E2B_HTTP_VERSION" : "httpVersion"} must be '1.1' or '2', got '${version}'`);
|
|
21
25
|
if (opts.debug)
|
|
22
26
|
throw new NotSupportedError("E2B's debug mode (a local envd)", "Remove debug: true; there is no local Runtime guest to connect to.");
|
|
23
27
|
}
|
package/dist/e2b/commands.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Process } from "../sandbox.js";
|
|
2
|
-
import type { ProcessInfo as RuntimeProcessInfo } from "../types.js";
|
|
2
|
+
import type { OutputEvent, ProcessInfo as RuntimeProcessInfo } from "../types.js";
|
|
3
3
|
import type { RequestOptions } from "../transport.js";
|
|
4
4
|
import type { RuntimeSandbox } from "./client.js";
|
|
5
5
|
import { TimeoutError, type CommandResult } from "./errors.js";
|
|
@@ -12,9 +12,9 @@ export interface CommandRequestOpts {
|
|
|
12
12
|
export interface CommandStartOpts extends CommandRequestOpts {
|
|
13
13
|
background?: boolean;
|
|
14
14
|
cwd?: string;
|
|
15
|
-
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
/** "user" (the default) is the sandbox's own user, `runtime`; any other
|
|
16
|
+
* user that exists in the sandbox, `root` included, runs it through
|
|
17
|
+
* `sudo -u`. An unknown user is refused, never created. */
|
|
18
18
|
user?: Username;
|
|
19
19
|
envs?: Record<string, string>;
|
|
20
20
|
onStdout?: (data: string) => void | Promise<void>;
|
|
@@ -47,7 +47,6 @@ export interface SandboxContext {
|
|
|
47
47
|
/** E2B's pids are numbers; Runtime's process ids are strings. The number is a
|
|
48
48
|
* 31-bit FNV-1a hash of the id, so every client derives the same one. */
|
|
49
49
|
export declare function pidOf(processId: string): number;
|
|
50
|
-
export declare function refuseUser(user: Username | undefined): void;
|
|
51
50
|
export declare function timeoutFor(timeoutMs: number | undefined): number;
|
|
52
51
|
/** One budget for lookup and mutation, including retries and queued input. */
|
|
53
52
|
export declare function commandRequest(opts: CommandRequestOpts, defaultTimeoutMs?: number): RequestOptions;
|
|
@@ -88,13 +87,23 @@ export declare class Commands {
|
|
|
88
87
|
/** Attaches to a running command; output from now on reaches the handle. */
|
|
89
88
|
connect(pid: number, opts?: CommandConnectOpts): Promise<CommandHandle>;
|
|
90
89
|
}
|
|
90
|
+
/** A command whose output this handle reads from the request that started
|
|
91
|
+
* it: its process id, the rest of its events, the controller that ends the
|
|
92
|
+
* read, and the Runtime process, fetched only when something needs it. */
|
|
93
|
+
export interface StartedStream {
|
|
94
|
+
started: true;
|
|
95
|
+
id: string;
|
|
96
|
+
events: AsyncIterator<OutputEvent>;
|
|
97
|
+
abort: AbortController;
|
|
98
|
+
process: () => Promise<Process>;
|
|
99
|
+
}
|
|
91
100
|
/** A command started in the background: E2B's CommandHandle over a Runtime
|
|
92
101
|
* process. Output streams into `stdout`/`stderr` and the callbacks as it
|
|
93
102
|
* arrives. */
|
|
94
103
|
export declare class CommandHandle {
|
|
95
104
|
#private;
|
|
96
105
|
readonly pid: number;
|
|
97
|
-
constructor(
|
|
106
|
+
constructor(source: Process | StartedStream, options: {
|
|
98
107
|
stdin: boolean;
|
|
99
108
|
timeoutMs: number;
|
|
100
109
|
cursor?: number;
|