withruntime 0.11.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 +7 -8
- package/E2B.md +16 -2
- package/README.md +3 -3
- package/README.src.md +3 -3
- package/VERCEL.md +3 -2
- package/compatibility-lock.json +55 -0
- 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 +39 -15
- package/dist/client.d.ts +5 -1
- package/dist/client.js +19 -3
- package/dist/compat/files.js +5 -1
- package/dist/compat/lease.d.ts +1 -0
- package/dist/compat/lease.js +14 -8
- package/dist/credentials.d.ts +9 -0
- package/dist/credentials.js +26 -4
- package/dist/daytona/daytona.js +29 -12
- package/dist/daytona/sandbox.d.ts +8 -3
- package/dist/daytona/sandbox.js +23 -4
- package/dist/e2b/sandbox.d.ts +7 -3
- package/dist/e2b/sandbox.js +21 -7
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +6 -0
- package/dist/identity.js +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/modal/index.js +6 -2
- package/dist/network-cli.js +7 -7
- package/dist/products/browser.d.ts +2 -1
- package/dist/products/browser.js +9 -19
- package/dist/products/desktop.d.ts +2 -1
- package/dist/products/desktop.js +5 -30
- 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 +1 -1
- package/dist/products/network-products.js +1 -1
- package/dist/products/observability.d.ts +1 -1
- package/dist/products/private-network.d.ts +1 -1
- package/dist/products/secrets.d.ts +2 -2
- package/dist/runloop/index.js +6 -2
- package/dist/sandbox.d.ts +27 -19
- package/dist/sandbox.js +121 -54
- 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 +54 -9
- package/dist/vercel/filesystem.js +3 -1
- package/dist/vercel/sandbox.d.ts +17 -10
- package/dist/vercel/sandbox.js +26 -13
- package/dist/wait.d.ts +12 -0
- package/dist/wait.js +47 -0
- package/package.json +1 -1
package/dist/compat/lease.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { RuntimeError } from "../errors.js";
|
|
2
|
-
/* A rival's sandbox may be asked to live longer than one Runtime
|
|
3
|
-
hour at most, ARCHITECTURE.md section 10): Vercel's `timeout` over an
|
|
4
|
-
Daytona's `autoStopInterval` over an hour
|
|
5
|
-
reaching toward the time asked for while their sandbox object lives,
|
|
6
|
-
extension at a time, never past that time.
|
|
2
|
+
/* A rival's sandbox may be asked to live longer than one Runtime time limit
|
|
3
|
+
(an hour at most, ARCHITECTURE.md section 10): Vercel's `timeout` over an
|
|
4
|
+
hour, Daytona's `autoStopInterval` over an hour. The drop-ins keep the
|
|
5
|
+
limit reaching toward the time asked for while their sandbox object lives,
|
|
6
|
+
one extension at a time, never past that time. A sandbox created with no
|
|
7
|
+
time limit renews itself on the server (0300; `endsAt` null) and is left
|
|
8
|
+
alone. */
|
|
7
9
|
/** The most one lease runs ahead of now. */
|
|
8
10
|
export const LEASE_MAX_SECONDS = 3600;
|
|
9
11
|
/** Whole seconds to add to a lease ending at `expiresAt` (epoch ms) so it
|
|
@@ -39,7 +41,10 @@ export class LeaseKeeper {
|
|
|
39
41
|
#schedule() {
|
|
40
42
|
if (this.#ended || this.#timer)
|
|
41
43
|
return;
|
|
42
|
-
const
|
|
44
|
+
const info = this.#sandbox().info;
|
|
45
|
+
if (info.endsAt === null)
|
|
46
|
+
return;
|
|
47
|
+
const expiresAt = Date.parse(info.endsAt ?? info.expiresAt);
|
|
43
48
|
if (!Number.isFinite(expiresAt) || this.#until() <= expiresAt)
|
|
44
49
|
return;
|
|
45
50
|
this.#timer = setTimeout(() => {
|
|
@@ -61,9 +66,10 @@ export class LeaseKeeper {
|
|
|
61
66
|
}
|
|
62
67
|
async #extend(now) {
|
|
63
68
|
const sandbox = this.#sandbox();
|
|
64
|
-
|
|
69
|
+
// No time limit: the server renews it, and an extension would change nothing.
|
|
70
|
+
if (sandbox.state !== "running" || sandbox.info.endsAt === null)
|
|
65
71
|
return;
|
|
66
|
-
const seconds = extensionSeconds(Date.parse(sandbox.info.expiresAt), this.#until(), Date.now(), now ? Infinity : this.#marginMs());
|
|
72
|
+
const seconds = extensionSeconds(Date.parse(sandbox.info.endsAt ?? sandbox.info.expiresAt), this.#until(), Date.now(), now ? Infinity : this.#marginMs());
|
|
67
73
|
if (seconds < 1)
|
|
68
74
|
return;
|
|
69
75
|
try {
|
package/dist/credentials.d.ts
CHANGED
|
@@ -10,6 +10,15 @@ export type SavedConnection = {
|
|
|
10
10
|
/** The account's name when it was saved, for `runtime account`. */
|
|
11
11
|
orgName?: string;
|
|
12
12
|
};
|
|
13
|
+
/** Saved accounts could not be updated because another command holds their
|
|
14
|
+
* lock: still `busy` after the whole wait, or `interrupted`, left by a command
|
|
15
|
+
* that died holding it. The message names the lock file to remove. */
|
|
16
|
+
export declare class CredentialsLockedError extends Error {
|
|
17
|
+
readonly lockFile: string;
|
|
18
|
+
readonly reason: "busy" | "interrupted";
|
|
19
|
+
readonly name = "CredentialsLockedError";
|
|
20
|
+
constructor(message: string, lockFile: string, reason: "busy" | "interrupted");
|
|
21
|
+
}
|
|
13
22
|
export declare function connectionOrigins(env: NodeJS.ProcessEnv): {
|
|
14
23
|
api: string;
|
|
15
24
|
auth: string;
|
package/dist/credentials.js
CHANGED
|
@@ -4,6 +4,23 @@ import { lstat, mkdir, open, rename, unlink } from "node:fs/promises";
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { isAbsolute, join } from "node:path";
|
|
6
6
|
import { apiOrigin, missingKey } from "./transport.js";
|
|
7
|
+
/** Saved accounts could not be updated because another command holds their
|
|
8
|
+
* lock: still `busy` after the whole wait, or `interrupted`, left by a command
|
|
9
|
+
* that died holding it. The message names the lock file to remove. */
|
|
10
|
+
export class CredentialsLockedError extends Error {
|
|
11
|
+
lockFile;
|
|
12
|
+
reason;
|
|
13
|
+
name = "CredentialsLockedError";
|
|
14
|
+
constructor(message, lockFile, reason) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.lockFile = lockFile;
|
|
17
|
+
this.reason = reason;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** How long a save waits for another command's update to finish. An update
|
|
21
|
+
* is one read, modify and write of small files, so only a heavily loaded
|
|
22
|
+
* machine comes near it. */
|
|
23
|
+
const LOCK_WAIT_MS = 30_000;
|
|
7
24
|
/** How many other accounts' connections one machine keeps beside the one in use. */
|
|
8
25
|
const SAVED_ACCOUNTS = 20;
|
|
9
26
|
export function connectionOrigins(env) {
|
|
@@ -107,7 +124,7 @@ export function connectionStore(env, directory = defaultDirectory(env)) {
|
|
|
107
124
|
* Never steal a lock: stale cleanup can otherwise delete a new live lock. */
|
|
108
125
|
async function updateAccounts(update) {
|
|
109
126
|
await privateDirectory(true);
|
|
110
|
-
const until = performance.now() +
|
|
127
|
+
const until = performance.now() + LOCK_WAIT_MS;
|
|
111
128
|
let handle;
|
|
112
129
|
for (;;) {
|
|
113
130
|
try {
|
|
@@ -127,16 +144,21 @@ export function connectionStore(env, directory = defaultDirectory(env)) {
|
|
|
127
144
|
// The holder may not have finished writing its process id yet.
|
|
128
145
|
}
|
|
129
146
|
if (typeof pid === "number" && Number.isSafeInteger(pid) && pid > 0) {
|
|
147
|
+
let gone = false;
|
|
130
148
|
try {
|
|
131
149
|
process.kill(pid, 0);
|
|
132
150
|
}
|
|
133
151
|
catch (error) {
|
|
134
|
-
|
|
135
|
-
throw new Error(`A previous Runtime credential update was interrupted. Remove ${lockFile} and retry.`);
|
|
152
|
+
gone = error.code === "ESRCH";
|
|
136
153
|
}
|
|
154
|
+
// A holder that finished between our read and our check has exited
|
|
155
|
+
// too, and its lock is gone or another's: wait for the lock again.
|
|
156
|
+
// Only a lock still naming the dead process was left by one that died.
|
|
157
|
+
if (gone && (await readPrivate(lockFile, 1024, "credential update lock")) === text)
|
|
158
|
+
throw new CredentialsLockedError(`A previous Runtime credential update was interrupted. Remove ${lockFile} and retry.`, lockFile, "interrupted");
|
|
137
159
|
}
|
|
138
160
|
if (performance.now() >= until)
|
|
139
|
-
throw new
|
|
161
|
+
throw new CredentialsLockedError(`Another Runtime command is updating saved accounts. Retry when it finishes. If no command is running, remove ${lockFile} and retry.`, lockFile, "busy");
|
|
140
162
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
141
163
|
}
|
|
142
164
|
try {
|
package/dist/daytona/daytona.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { clientFor, } from "./client.js";
|
|
2
2
|
import { DaytonaError, DaytonaNotFoundError, guard, NotSupportedError } from "./errors.js";
|
|
3
|
-
import {
|
|
3
|
+
import { LEASE_MAX_SECONDS } from "../compat/lease.js";
|
|
4
|
+
import { idlePauseOf, idleSeconds, networkRules, notFound, retentionDays, Sandbox, windowSeconds, } from "./sandbox.js";
|
|
4
5
|
/* Daytona's client over Runtime's. `new Daytona()` then `daytona.create()`
|
|
5
6
|
makes a Runtime sandbox with Daytona's defaults. */
|
|
6
7
|
/** Daytona's default machine: 1 vCPU, 1 GiB memory, 3 GiB disk (checked
|
|
@@ -211,9 +212,15 @@ export class Daytona {
|
|
|
211
212
|
memoryMiB: Math.round(resources.memory * 1024),
|
|
212
213
|
diskMiB: Math.round(resources.disk * 1024),
|
|
213
214
|
}),
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
215
|
+
// A time to live is a time limit, kept by the lease. Without one there
|
|
216
|
+
// is none: it runs while it works and autoStopInterval is its idle
|
|
217
|
+
// pause, counted by the sandbox itself, so a long command is never
|
|
218
|
+
// frozen for want of calls from this client (0300).
|
|
219
|
+
...(lifecycle.deadline
|
|
220
|
+
? {
|
|
221
|
+
timeoutSeconds: Math.max(60, Math.min(lifecycle.windowSeconds, Math.floor((lifecycle.deadline - Date.now()) / 1000))),
|
|
222
|
+
}
|
|
223
|
+
: { idlePauseSeconds: idlePauseOf(lifecycle.autoStopInterval) }),
|
|
217
224
|
onLeaseEnd: lifecycle.ephemeral || lifecycle.deadline ? "stop" : "pause",
|
|
218
225
|
...(params.name ? { name: params.name } : {}),
|
|
219
226
|
...(labels ? { labels } : {}),
|
|
@@ -303,14 +310,24 @@ export class Daytona {
|
|
|
303
310
|
language: runtime.info.labels[LANGUAGE_LABEL] ?? "python",
|
|
304
311
|
public: false,
|
|
305
312
|
...(user ? { user } : {}),
|
|
306
|
-
lifecycle:
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
313
|
+
lifecycle: runtime.info.timeoutSeconds
|
|
314
|
+
? {
|
|
315
|
+
windowSeconds: runtime.info.timeoutSeconds,
|
|
316
|
+
idleSeconds: runtime.info.timeoutSeconds,
|
|
317
|
+
ephemeral: onLeaseEnd === "stop",
|
|
318
|
+
autoStopInterval: Math.round(runtime.info.timeoutSeconds / 60),
|
|
319
|
+
autoArchiveInterval: 0,
|
|
320
|
+
autoDeleteInterval: -1,
|
|
321
|
+
}
|
|
322
|
+
: {
|
|
323
|
+
// No time limit: its idle pause is its autoStopInterval.
|
|
324
|
+
windowSeconds: LEASE_MAX_SECONDS,
|
|
325
|
+
idleSeconds: Infinity,
|
|
326
|
+
ephemeral: onLeaseEnd === "stop",
|
|
327
|
+
autoStopInterval: Math.round((runtime.info.idlePauseSeconds ?? 0) / 60),
|
|
328
|
+
autoArchiveInterval: 0,
|
|
329
|
+
autoDeleteInterval: -1,
|
|
330
|
+
},
|
|
314
331
|
});
|
|
315
332
|
}
|
|
316
333
|
/** Live and stopped (paused) sandboxes, oldest first. Await it for the old
|
|
@@ -134,8 +134,9 @@ export declare class Sandbox {
|
|
|
134
134
|
refreshData(): Promise<void>;
|
|
135
135
|
/** Counts as activity: moves the lease a full autoStop window on. */
|
|
136
136
|
refreshActivity(): Promise<void>;
|
|
137
|
-
/** The sandbox pauses after `interval` minutes
|
|
138
|
-
*
|
|
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. */
|
|
139
140
|
setAutostopInterval(interval: number): Promise<void>;
|
|
140
141
|
setAutoPauseInterval(interval: number): Promise<void>;
|
|
141
142
|
/** Runtime has no archive tier; recorded only. */
|
|
@@ -192,8 +193,12 @@ export declare class Sandbox {
|
|
|
192
193
|
}
|
|
193
194
|
/** Daytona's autoStopInterval (minutes; 0 is off) as the lease window. */
|
|
194
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;
|
|
195
200
|
/** Daytona's autoStopInterval (minutes; 0 is never) as seconds without a
|
|
196
|
-
* call before
|
|
201
|
+
* call before a sandbox with a time to live pauses. */
|
|
197
202
|
export declare function idleSeconds(autoStopMinutes: number): number;
|
|
198
203
|
/** Minutes as Runtime's retention: whole days, 1 to 365. */
|
|
199
204
|
export declare function retentionDays(minutes: number): number;
|
package/dist/daytona/sandbox.js
CHANGED
|
@@ -238,7 +238,11 @@ export class Sandbox {
|
|
|
238
238
|
if (state === "stopped" || state === "stopping")
|
|
239
239
|
throw new DaytonaError(`Sandbox ${this.id} was deleted and cannot start.`, 410);
|
|
240
240
|
if (state === "paused" || state === "pausing")
|
|
241
|
-
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
|
+
: {}));
|
|
242
246
|
this.#pausedByPause = false;
|
|
243
247
|
}
|
|
244
248
|
/** Stops the sandbox: Runtime pauses it, keeping its files and memory, so
|
|
@@ -291,11 +295,16 @@ export class Sandbox {
|
|
|
291
295
|
async refreshActivity() {
|
|
292
296
|
await guard("sandbox", () => this.#live(true));
|
|
293
297
|
}
|
|
294
|
-
/** The sandbox pauses after `interval` minutes
|
|
295
|
-
*
|
|
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. */
|
|
296
301
|
async setAutostopInterval(interval) {
|
|
297
302
|
const lifecycle = this.#options.lifecycle;
|
|
298
303
|
lifecycle.autoStopInterval = interval;
|
|
304
|
+
if (!this.#rt.info.timeoutSeconds) {
|
|
305
|
+
await guard("sandbox", () => this.#rt.update({ idlePauseSeconds: idlePauseOf(interval) }));
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
299
308
|
lifecycle.windowSeconds = windowSeconds(interval);
|
|
300
309
|
lifecycle.idleSeconds = idleSeconds(interval);
|
|
301
310
|
await guard("sandbox", () => this.#live());
|
|
@@ -321,6 +330,8 @@ export class Sandbox {
|
|
|
321
330
|
delete this.#options.lifecycle.deadline;
|
|
322
331
|
return;
|
|
323
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.");
|
|
324
335
|
const deadline = Date.parse(this.#rt.info.createdAt) + ttlMinutes * 60_000;
|
|
325
336
|
if (Date.parse(this.#rt.info.expiresAt) > deadline)
|
|
326
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.");
|
|
@@ -444,8 +455,16 @@ export function windowSeconds(autoStopMinutes) {
|
|
|
444
455
|
return 3600;
|
|
445
456
|
return Math.min(3600, Math.max(60, Math.round(autoStopMinutes * 60)));
|
|
446
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
|
+
}
|
|
447
466
|
/** Daytona's autoStopInterval (minutes; 0 is never) as seconds without a
|
|
448
|
-
* call before
|
|
467
|
+
* call before a sandbox with a time to live pauses. */
|
|
449
468
|
export function idleSeconds(autoStopMinutes) {
|
|
450
469
|
if (autoStopMinutes <= 0)
|
|
451
470
|
return Infinity;
|
package/dist/e2b/sandbox.d.ts
CHANGED
|
@@ -9,7 +9,9 @@ export declare const DEFAULT_SHAPE: {
|
|
|
9
9
|
readonly vcpu: 2;
|
|
10
10
|
readonly memoryMiB: 512;
|
|
11
11
|
};
|
|
12
|
-
/** E2B's default sandbox timeout, 300 s.
|
|
12
|
+
/** E2B's default sandbox timeout, 300 s. Not sent: a sandbox created with no
|
|
13
|
+
* timeoutMs has no time limit on Runtime, running while it works and pausing
|
|
14
|
+
* when idle (0300). Kept for code that imports it. */
|
|
13
15
|
export declare const DEFAULT_TIMEOUT_MS = 300000;
|
|
14
16
|
export type SandboxState = "running" | "paused";
|
|
15
17
|
export type SandboxOnTimeout = "pause" | "kill" | {
|
|
@@ -30,7 +32,8 @@ export interface SandboxOpts extends ConnectionOpts {
|
|
|
30
32
|
/** The sandbox's own environment: every command, terminal and code run in it
|
|
31
33
|
* gets these, whoever connects. Values are never shown again. */
|
|
32
34
|
envs?: Record<string, string>;
|
|
33
|
-
/**
|
|
35
|
+
/** When it ends, at most 24 hours. Leave it out for no time limit: it runs
|
|
36
|
+
* while it works and pauses when idle. Runtime's limits run 60 s to 1 hour:
|
|
34
37
|
* shorter rounds up to 60 s (with a warning); longer gets an hour, moved on
|
|
35
38
|
* while this process runs, up to the time asked for. */
|
|
36
39
|
timeoutMs?: number;
|
|
@@ -125,7 +128,8 @@ export declare class Sandbox {
|
|
|
125
128
|
kill(opts?: Pick<ConnectionOpts, "requestTimeoutMs" | "signal">): Promise<boolean>;
|
|
126
129
|
/** Sets the sandbox to end `timeoutMs` from now, up to 24 hours; past an
|
|
127
130
|
* hour the lease is moved on while this process runs. Runtime cannot end a
|
|
128
|
-
* lease early, so an end sooner than the lease's is refused.
|
|
131
|
+
* lease early, so an end sooner than the lease's is refused. A sandbox
|
|
132
|
+
* created with no timeout has no end to move, and nothing changes. */
|
|
129
133
|
static setTimeout(sandboxId: string, timeoutMs: number, opts?: SandboxApiOpts): Promise<void>;
|
|
130
134
|
setTimeout(timeoutMs: number, opts?: Pick<ConnectionOpts, "requestTimeoutMs" | "signal">): Promise<void>;
|
|
131
135
|
static getInfo(sandboxId: string, opts?: SandboxApiOpts): Promise<SandboxInfo>;
|
package/dist/e2b/sandbox.js
CHANGED
|
@@ -9,7 +9,9 @@ import { Pty } from "./pty.js";
|
|
|
9
9
|
/** E2B's default machine: 2 vCPU and 512 MiB (docs.e2b.dev/billing, checked
|
|
10
10
|
* 23 September 2026). Pass `runtime: { create: { memoryMiB } }` for another. */
|
|
11
11
|
export const DEFAULT_SHAPE = { vcpu: 2, memoryMiB: 512 };
|
|
12
|
-
/** E2B's default sandbox timeout, 300 s.
|
|
12
|
+
/** E2B's default sandbox timeout, 300 s. Not sent: a sandbox created with no
|
|
13
|
+
* timeoutMs has no time limit on Runtime, running while it works and pausing
|
|
14
|
+
* when idle (0300). Kept for code that imports it. */
|
|
13
15
|
export const DEFAULT_TIMEOUT_MS = 300_000;
|
|
14
16
|
/** Runtime's lease bounds (MAX_TIMEOUT_SECONDS in the API). */
|
|
15
17
|
const MIN_LEASE_SECONDS = 60;
|
|
@@ -93,6 +95,8 @@ async function renew(keeper) {
|
|
|
93
95
|
if (current === "stopped")
|
|
94
96
|
return stopKeeping(id);
|
|
95
97
|
if (current === "running") {
|
|
98
|
+
if (keeper.runtime.info.endsAt === null)
|
|
99
|
+
return stopKeeping(id);
|
|
96
100
|
const end = Date.parse(keeper.runtime.info.expiresAt);
|
|
97
101
|
const want = Math.min(keeper.until, Date.now() + KEEP_AHEAD_MS);
|
|
98
102
|
const later = Math.floor((want - end) / 1000);
|
|
@@ -200,7 +204,8 @@ function infoOf(runtime) {
|
|
|
200
204
|
...(info.name ? { name: info.name } : {}),
|
|
201
205
|
metadata: info.labels ?? {},
|
|
202
206
|
startedAt: new Date(info.createdAt),
|
|
203
|
-
|
|
207
|
+
// With no time limit, where it is paid up to: always ahead, moving on.
|
|
208
|
+
endAt: new Date(info.endsAt ?? info.expiresAt),
|
|
204
209
|
state: current === "stopped" ? "paused" : current,
|
|
205
210
|
cpuCount: info.vcpu,
|
|
206
211
|
memoryMB: info.memoryMiB,
|
|
@@ -301,9 +306,10 @@ export class Sandbox {
|
|
|
301
306
|
options.signal?.throwIfAborted();
|
|
302
307
|
refuseCreate(opts);
|
|
303
308
|
const client = clientFor(opts);
|
|
304
|
-
|
|
309
|
+
// No timeoutMs, no time limit: it runs while it works (0300).
|
|
310
|
+
const asked = opts.timeoutMs === undefined ? undefined : Date.now() + opts.timeoutMs;
|
|
305
311
|
const input = {
|
|
306
|
-
timeoutSeconds: leaseSeconds(opts.timeoutMs
|
|
312
|
+
...(opts.timeoutMs === undefined ? {} : { timeoutSeconds: leaseSeconds(opts.timeoutMs) }),
|
|
307
313
|
onLeaseEnd: onLeaseEnd(opts.lifecycle),
|
|
308
314
|
// With a lifecycle, E2B resumes a paused sandbox on traffic only when
|
|
309
315
|
// asked (autoResume); Runtime's automatic wake is the same thing (0093).
|
|
@@ -324,7 +330,8 @@ export class Sandbox {
|
|
|
324
330
|
...opts.runtime?.create,
|
|
325
331
|
};
|
|
326
332
|
const runtime = await guard("sandbox", () => client.sandboxes.create(create, options));
|
|
327
|
-
|
|
333
|
+
if (asked !== undefined)
|
|
334
|
+
keepUntil(runtime, asked);
|
|
328
335
|
return new this(runtime, client, opts.requestTimeoutMs);
|
|
329
336
|
}
|
|
330
337
|
/** Connects to a sandbox by id, waking it if it is paused, as E2B does. A
|
|
@@ -373,7 +380,8 @@ export class Sandbox {
|
|
|
373
380
|
}
|
|
374
381
|
/** Sets the sandbox to end `timeoutMs` from now, up to 24 hours; past an
|
|
375
382
|
* hour the lease is moved on while this process runs. Runtime cannot end a
|
|
376
|
-
* lease early, so an end sooner than the lease's is refused.
|
|
383
|
+
* lease early, so an end sooner than the lease's is refused. A sandbox
|
|
384
|
+
* created with no timeout has no end to move, and nothing changes. */
|
|
377
385
|
static async setTimeout(sandboxId, timeoutMs, opts = {}) {
|
|
378
386
|
const client = clientFor(opts);
|
|
379
387
|
const runtime = await guard("sandbox", () => client.sandboxes.get(sandboxId, request(opts)));
|
|
@@ -547,6 +555,9 @@ async function resume(runtime, opts) {
|
|
|
547
555
|
}
|
|
548
556
|
else if (opts.timeoutMs !== undefined) {
|
|
549
557
|
checkTimeout(opts.timeoutMs);
|
|
558
|
+
// No time limit: no end to move (0300).
|
|
559
|
+
if (runtime.info.endsAt === null)
|
|
560
|
+
return;
|
|
550
561
|
const later = Math.min(asked, Date.now() + KEEP_AHEAD_MS) - Date.parse(runtime.info.expiresAt);
|
|
551
562
|
if (later > 1000)
|
|
552
563
|
await guard("sandbox", () => runtime.extend(Math.ceil(later / 1000), request(opts)));
|
|
@@ -562,8 +573,11 @@ async function stop(runtime, opts) {
|
|
|
562
573
|
}
|
|
563
574
|
async function extendTo(runtime, timeoutMs, opts) {
|
|
564
575
|
checkTimeout(timeoutMs);
|
|
576
|
+
// No time limit: no end to move (0300).
|
|
577
|
+
if (runtime.info.endsAt === null)
|
|
578
|
+
return;
|
|
565
579
|
const asked = Date.now() + timeoutMs;
|
|
566
|
-
const end = Date.parse(runtime.info.expiresAt);
|
|
580
|
+
const end = Date.parse(runtime.info.endsAt ?? runtime.info.expiresAt);
|
|
567
581
|
if (asked - end < -1000)
|
|
568
582
|
throw new NotSupportedError("Ending a sandbox sooner than its lease (a shorter setTimeout)", "Runtime cannot end a lease early. Call kill() when the work is done.");
|
|
569
583
|
const later = Math.min(asked, Date.now() + KEEP_AHEAD_MS) - end;
|
package/dist/errors.d.ts
CHANGED
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
* packages/cloud/src/api/respond.ts) and retrying cannot change them.
|
|
7
7
|
* host_unavailable and busy pass, and are retried. */
|
|
8
8
|
export declare const DELIBERATE: ReadonlySet<string>;
|
|
9
|
+
/** Refusals that guarantee nothing ran: a sandbox running as many commands
|
|
10
|
+
* as it can (`guest_busy`), and the API's own limits, which answer before any
|
|
11
|
+
* work starts (`busy`, `rate_limited`). Sending such a call again is always
|
|
12
|
+
* safe, so the SDK does, with backoff, until the call's deadline (five
|
|
13
|
+
* minutes when it has none), however many retries it allows otherwise. */
|
|
14
|
+
export declare const NOTHING_RAN: ReadonlySet<string>;
|
|
9
15
|
/** Refusals of a create that clear when a sandbox stops or pauses, or a host
|
|
10
16
|
* frees room: the trial's slots, an email domain's trial slots, the account's
|
|
11
17
|
* quota and the region's capacity. `sandboxes.create` waits them out, retrying
|
package/dist/errors.js
CHANGED
|
@@ -16,6 +16,12 @@ export const DELIBERATE = new Set([
|
|
|
16
16
|
"identity_unavailable",
|
|
17
17
|
"env_unavailable",
|
|
18
18
|
]);
|
|
19
|
+
/** Refusals that guarantee nothing ran: a sandbox running as many commands
|
|
20
|
+
* as it can (`guest_busy`), and the API's own limits, which answer before any
|
|
21
|
+
* work starts (`busy`, `rate_limited`). Sending such a call again is always
|
|
22
|
+
* safe, so the SDK does, with backoff, until the call's deadline (five
|
|
23
|
+
* minutes when it has none), however many retries it allows otherwise. */
|
|
24
|
+
export const NOTHING_RAN = new Set(["guest_busy", "busy", "rate_limited"]);
|
|
19
25
|
/** Refusals that pass on their own: a host frees room, a trial slot frees. */
|
|
20
26
|
const PASSING = new Set(["no_capacity", "trial_busy"]);
|
|
21
27
|
/** Refusals of a create that clear when a sandbox stops or pauses, or a host
|
package/dist/identity.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RuntimeError } from "./errors.js";
|
|
2
|
+
import { sandboxMarker } from "./transport.js";
|
|
2
3
|
async function requestCredentials() {
|
|
3
4
|
let url = process.env.RUNTIME_ID_TOKEN_REQUEST_URL;
|
|
4
5
|
let token = process.env.RUNTIME_ID_TOKEN_REQUEST_TOKEN;
|
|
@@ -7,7 +8,7 @@ async function requestCredentials() {
|
|
|
7
8
|
try {
|
|
8
9
|
// Loaded here, so the SDK still loads where there is no file system.
|
|
9
10
|
const { readFile } = await import("node:fs/promises");
|
|
10
|
-
const env = JSON.parse(await readFile(
|
|
11
|
+
const env = JSON.parse(await readFile(sandboxMarker.path, "utf8"));
|
|
11
12
|
url = env.RUNTIME_ID_TOKEN_REQUEST_URL;
|
|
12
13
|
token = env.RUNTIME_ID_TOKEN_REQUEST_TOKEN;
|
|
13
14
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -35,7 +35,8 @@ export type * from "./types.js";
|
|
|
35
35
|
* does the same with an explicit client. */
|
|
36
36
|
export declare class Sandbox extends SandboxClass {
|
|
37
37
|
/** Creates a sandbox (every field optional) and waits until it is running.
|
|
38
|
-
*
|
|
38
|
+
* With no `timeoutSeconds` it runs while it works and pauses when idle.
|
|
39
|
+
* `keepAlive: true` (or its options) carries one with a time limit past it
|
|
39
40
|
* until you stop it; see `sandbox.keepAlive()`. */
|
|
40
41
|
static create(input?: CreateSandbox & {
|
|
41
42
|
wait?: boolean;
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,8 @@ export * from "./errors.js";
|
|
|
14
14
|
* does the same with an explicit client. */
|
|
15
15
|
export class Sandbox extends SandboxClass {
|
|
16
16
|
/** Creates a sandbox (every field optional) and waits until it is running.
|
|
17
|
-
*
|
|
17
|
+
* With no `timeoutSeconds` it runs while it works and pauses when idle.
|
|
18
|
+
* `keepAlive: true` (or its options) carries one with a time limit past it
|
|
18
19
|
* until you stop it; see `sandbox.keepAlive()`. */
|
|
19
20
|
static async create(input = {}, options = {}) {
|
|
20
21
|
const { client, ...rest } = options;
|
package/dist/modal/index.js
CHANGED
|
@@ -164,8 +164,12 @@ export class SandboxService {
|
|
|
164
164
|
vcpu: params.cpu,
|
|
165
165
|
memoryMiB: params.memoryMiB,
|
|
166
166
|
region: placement,
|
|
167
|
-
|
|
168
|
-
|
|
167
|
+
// A timeout is the customer's limit, and the sandbox never pauses
|
|
168
|
+
// for idleness before it. None is no time limit (0300): it runs while
|
|
169
|
+
// it works and pauses when idle.
|
|
170
|
+
...(params.timeoutMs === undefined
|
|
171
|
+
? {}
|
|
172
|
+
: { timeoutSeconds: Math.ceil(params.timeoutMs / 1000), idlePauseSeconds: 0 }),
|
|
169
173
|
onLeaseEnd: "stop",
|
|
170
174
|
network: {
|
|
171
175
|
internet: !params.blockNetwork,
|
package/dist/network-cli.js
CHANGED
|
@@ -25,7 +25,7 @@ export const NETWORK_HELP = {
|
|
|
25
25
|
ls Your domains
|
|
26
26
|
get <hostname> One domain and its DNS records
|
|
27
27
|
rm <hostname> Stop serving it
|
|
28
|
-
|
|
28
|
+
Needs a kept top-up. The first visit gets the certificate, in a few seconds.
|
|
29
29
|
`,
|
|
30
30
|
port: `runtime port <command>
|
|
31
31
|
|
|
@@ -34,7 +34,7 @@ export const NETWORK_HELP = {
|
|
|
34
34
|
Prints address:port for your client
|
|
35
35
|
ls [--sandbox <id>] Your open ports
|
|
36
36
|
close <portId> Close one, its open connections too
|
|
37
|
-
|
|
37
|
+
Needs a kept top-up. TCP, carried as it is: use your service's own password and TLS.
|
|
38
38
|
`,
|
|
39
39
|
address: `runtime address <command>
|
|
40
40
|
|
|
@@ -42,7 +42,7 @@ export const NETWORK_HELP = {
|
|
|
42
42
|
account sends from it, for allow-lists
|
|
43
43
|
ls Your addresses
|
|
44
44
|
release <addressId> Give it back; sandboxes use the shared addresses again
|
|
45
|
-
|
|
45
|
+
Needs credit on the account.
|
|
46
46
|
`,
|
|
47
47
|
tunnel: `runtime tunnel <command>
|
|
48
48
|
|
|
@@ -56,7 +56,7 @@ export const NETWORK_HELP = {
|
|
|
56
56
|
peer rotate <peerId> [--out runtime.conf]
|
|
57
57
|
A new key; the old one stops working within seconds
|
|
58
58
|
peer rm <peerId> Remove it; its tunnel ends within seconds
|
|
59
|
-
--route: your own ranges behind the peer that sandboxes may reach.
|
|
59
|
+
--route: your own ranges behind the peer that sandboxes may reach. Needs a kept top-up.
|
|
60
60
|
`,
|
|
61
61
|
network: `runtime network <command>
|
|
62
62
|
|
|
@@ -69,13 +69,13 @@ export const NETWORK_HELP = {
|
|
|
69
69
|
upstream-proxy get The proxy in use
|
|
70
70
|
upstream-proxy remove Connections leave directly again
|
|
71
71
|
A proxy that refuses or cannot be reached fails the connection; nothing goes
|
|
72
|
-
around it.
|
|
72
|
+
around it. Needs a kept top-up.
|
|
73
73
|
|
|
74
74
|
private on Your sandboxes reach each other by name, over TCP:
|
|
75
75
|
<name>.sandbox.internal:<port> or <id>.sandbox.internal:<port>
|
|
76
76
|
private off They stop; open connections between them are cut
|
|
77
77
|
private status On or off
|
|
78
|
-
Only your own sandboxes answer.
|
|
78
|
+
Only your own sandboxes answer. Needs a kept top-up; free.
|
|
79
79
|
`,
|
|
80
80
|
};
|
|
81
81
|
function usage(product, message) {
|
|
@@ -411,7 +411,7 @@ export async function networkProductCommand(product, argv, args, out, client) {
|
|
|
411
411
|
? "On: your sandboxes reach each other at <name>.sandbox.internal:<port> over TCP."
|
|
412
412
|
: state.allowed
|
|
413
413
|
? "Off. Turn it on: runtime network private on"
|
|
414
|
-
: "Off. Reaching your sandboxes by name
|
|
414
|
+
: "Off. Reaching your sandboxes by name needs a kept top-up.";
|
|
415
415
|
switch (rest[0]) {
|
|
416
416
|
case "on":
|
|
417
417
|
case "off": {
|
|
@@ -34,7 +34,8 @@ export type BrowserStartOptions = {
|
|
|
34
34
|
export declare function sandboxBrowser(t: Transport, sandbox: Sandbox): {
|
|
35
35
|
/** Starts Chromium and returns its CDP address. The first start in a
|
|
36
36
|
* sandbox installs Chromium, about a minute; this waits for it (up to 10
|
|
37
|
-
* minutes). Starting a running browser returns it
|
|
37
|
+
* minutes, or the call's timeoutMs). Starting a running browser returns it
|
|
38
|
+
* as it is. */
|
|
38
39
|
start(input?: BrowserStartOptions, options?: RequestOptions): Promise<{
|
|
39
40
|
running: true;
|
|
40
41
|
headless: boolean;
|
package/dist/products/browser.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { whileInstalling } from "../wait.js";
|
|
1
2
|
/** `sbx.browser`: Chromium in the sandbox, for agents that speak CDP.
|
|
2
3
|
*
|
|
3
4
|
* const { cdpUrl } = await sbx.browser.start();
|
|
@@ -9,26 +10,15 @@ export function sandboxBrowser(t, sandbox) {
|
|
|
9
10
|
return {
|
|
10
11
|
/** Starts Chromium and returns its CDP address. The first start in a
|
|
11
12
|
* sandbox installs Chromium, about a minute; this waits for it (up to 10
|
|
12
|
-
* minutes). Starting a running browser returns it
|
|
13
|
+
* minutes, or the call's timeoutMs). Starting a running browser returns it
|
|
14
|
+
* as it is. */
|
|
13
15
|
async start(input = {}, options) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
body: input,
|
|
21
|
-
...options,
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
catch (error) {
|
|
25
|
-
const code = error.code;
|
|
26
|
-
if ((code !== "browser_installing" && code !== "desktop_installing") ||
|
|
27
|
-
Date.now() > deadline)
|
|
28
|
-
throw error;
|
|
29
|
-
await new Promise((resolve) => setTimeout(resolve, error.retryAfterMs ?? 10_000));
|
|
30
|
-
}
|
|
31
|
-
}
|
|
16
|
+
return whileInstalling(["browser_installing", "desktop_installing"], options, (request) => t.json({
|
|
17
|
+
method: "POST",
|
|
18
|
+
path: `${base()}:start`,
|
|
19
|
+
body: input,
|
|
20
|
+
...request,
|
|
21
|
+
}));
|
|
32
22
|
},
|
|
33
23
|
/** Whether it runs, and a fresh cdpUrl when it does. */
|
|
34
24
|
get: (options) => t.json({ method: "GET", path: base(), ...options }),
|
|
@@ -53,7 +53,8 @@ export type RecordOptions = {
|
|
|
53
53
|
* Coordinates are pixels from the top left of the screen. */
|
|
54
54
|
export declare function sandboxDesktop(t: Transport, sandbox: Sandbox): {
|
|
55
55
|
/** Starts the desktop. The first start in a sandbox installs it, which
|
|
56
|
-
* takes a minute or two; this waits for that (up to 10 minutes
|
|
56
|
+
* takes a minute or two; this waits for that (up to 10 minutes, or the
|
|
57
|
+
* call's timeoutMs). */
|
|
57
58
|
start(size?: {
|
|
58
59
|
width?: number;
|
|
59
60
|
height?: number;
|