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/e2b/sandbox.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { clientFor, request, } from "./client.js";
|
|
2
2
|
import { Commands } from "./commands.js";
|
|
3
|
-
import { guard, InvalidArgumentError, NotSupportedError, SandboxError, SandboxNotFoundError, TemplateError, } from "./errors.js";
|
|
3
|
+
import { guard, InvalidArgumentError, NotSupportedError, PublicPreviewNotAllowedError, SandboxError, SandboxNotFoundError, TemplateError, } from "./errors.js";
|
|
4
4
|
import { Filesystem } from "./filesystem.js";
|
|
5
5
|
import { Pty } from "./pty.js";
|
|
6
6
|
/* E2B's Sandbox over Runtime's. Every call is one or a few calls of the
|
|
@@ -9,21 +9,30 @@ 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;
|
|
16
18
|
const MAX_LEASE_SECONDS = 3600;
|
|
19
|
+
/** E2B's longest sandbox timeout, 24 hours (its Pro plan). */
|
|
20
|
+
const LONGEST_TIMEOUT_MS = 86_400_000;
|
|
17
21
|
/** E2B template names that mean "the stock environment": Runtime's default image. */
|
|
18
22
|
const STOCK_TEMPLATES = new Set(["base", "code-interpreter-v1", "code-interpreter"]);
|
|
19
23
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
20
24
|
let warnedShortLease = false;
|
|
25
|
+
/** E2B's own bound since 2.52.0: refused before anything happens. */
|
|
26
|
+
const MAX_FORK_COUNT = 20;
|
|
27
|
+
function checkForkCount(count) {
|
|
28
|
+
if (count !== undefined && (!Number.isInteger(count) || count < 1 || count > MAX_FORK_COUNT))
|
|
29
|
+
throw new InvalidArgumentError(`count must be an integer between 1 and ${MAX_FORK_COUNT}`);
|
|
30
|
+
}
|
|
31
|
+
/** The first lease for `timeoutMs`: up to an hour; `keepUntil` carries a
|
|
32
|
+
* longer timeout on from there. */
|
|
21
33
|
function leaseSeconds(timeoutMs) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const seconds = Math.ceil(timeoutMs / 1000);
|
|
25
|
-
if (seconds > MAX_LEASE_SECONDS)
|
|
26
|
-
throw new NotSupportedError(`A sandbox timeout of ${timeoutMs} ms (over one hour)`, "Runtime leases last up to an hour (3_600_000 ms); call sandbox.setTimeout(...) before it ends to keep going, as often as needed.");
|
|
34
|
+
checkTimeout(timeoutMs);
|
|
35
|
+
const seconds = Math.min(Math.ceil(timeoutMs / 1000), MAX_LEASE_SECONDS);
|
|
27
36
|
if (seconds < MIN_LEASE_SECONDS) {
|
|
28
37
|
if (!warnedShortLease && typeof process !== "undefined") {
|
|
29
38
|
warnedShortLease = true;
|
|
@@ -33,6 +42,87 @@ function leaseSeconds(timeoutMs) {
|
|
|
33
42
|
}
|
|
34
43
|
return seconds;
|
|
35
44
|
}
|
|
45
|
+
function checkTimeout(timeoutMs) {
|
|
46
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
47
|
+
throw new InvalidArgumentError(`timeoutMs must be a positive number, not ${timeoutMs}.`);
|
|
48
|
+
if (timeoutMs > LONGEST_TIMEOUT_MS)
|
|
49
|
+
throw new InvalidArgumentError(`timeoutMs is at most 24 hours (86_400_000 ms), as on E2B, not ${timeoutMs}.`);
|
|
50
|
+
}
|
|
51
|
+
/* Timeouts over an hour. Runtime's lease reaches at most an hour ahead of
|
|
52
|
+
now, and E2B's timeout up to a day. While this process runs, the adapter
|
|
53
|
+
moves the lease on every five minutes, as far as an hour ahead and never
|
|
54
|
+
past the end asked for. One keeper per sandbox, whichever object asked.
|
|
55
|
+
If the process ends first, the sandbox ends when its lease does, at most
|
|
56
|
+
an hour later; it is never kept past the time asked for. */
|
|
57
|
+
const KEEP_EVERY_MS = 5 * 60_000;
|
|
58
|
+
/** Kept under the API's hour, for the clocks and the trip. */
|
|
59
|
+
const KEEP_AHEAD_MS = (MAX_LEASE_SECONDS - 30) * 1000;
|
|
60
|
+
const keepers = new Map();
|
|
61
|
+
let warnedKeeper = false;
|
|
62
|
+
/** Keeps `runtime` until `until` (epoch ms), or stops keeping it when that
|
|
63
|
+
* is within the hour its lease can already reach. */
|
|
64
|
+
function keepUntil(runtime, until) {
|
|
65
|
+
const keeper = keepers.get(runtime.id);
|
|
66
|
+
if (until - Date.now() <= MAX_LEASE_SECONDS * 1000)
|
|
67
|
+
return stopKeeping(runtime.id);
|
|
68
|
+
if (keeper) {
|
|
69
|
+
keeper.until = until;
|
|
70
|
+
keeper.runtime = runtime;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const made = { until, runtime };
|
|
74
|
+
keepers.set(runtime.id, made);
|
|
75
|
+
schedule(made);
|
|
76
|
+
}
|
|
77
|
+
function stopKeeping(sandboxId) {
|
|
78
|
+
const keeper = keepers.get(sandboxId);
|
|
79
|
+
if (keeper?.timer !== undefined)
|
|
80
|
+
clearTimeout(keeper.timer);
|
|
81
|
+
keepers.delete(sandboxId);
|
|
82
|
+
}
|
|
83
|
+
function schedule(keeper) {
|
|
84
|
+
keeper.timer = setTimeout(() => void renew(keeper), KEEP_EVERY_MS);
|
|
85
|
+
// Never what keeps a Node or Bun process running.
|
|
86
|
+
keeper.timer.unref?.();
|
|
87
|
+
}
|
|
88
|
+
async function renew(keeper) {
|
|
89
|
+
const id = keeper.runtime.id;
|
|
90
|
+
if (keepers.get(id) !== keeper)
|
|
91
|
+
return;
|
|
92
|
+
try {
|
|
93
|
+
await keeper.runtime.refresh();
|
|
94
|
+
const current = state(keeper.runtime);
|
|
95
|
+
if (current === "stopped")
|
|
96
|
+
return stopKeeping(id);
|
|
97
|
+
if (current === "running") {
|
|
98
|
+
if (keeper.runtime.info.endsAt === null)
|
|
99
|
+
return stopKeeping(id);
|
|
100
|
+
const end = Date.parse(keeper.runtime.info.expiresAt);
|
|
101
|
+
const want = Math.min(keeper.until, Date.now() + KEEP_AHEAD_MS);
|
|
102
|
+
const later = Math.floor((want - end) / 1000);
|
|
103
|
+
if (later >= 1)
|
|
104
|
+
await keeper.runtime.extend(later);
|
|
105
|
+
if (Date.parse(keeper.runtime.info.expiresAt) >= keeper.until - 1000)
|
|
106
|
+
return stopKeeping(id);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (!warnedKeeper && typeof process !== "undefined" && process.emitWarning) {
|
|
111
|
+
warnedKeeper = true;
|
|
112
|
+
process.emitWarning(`Could not extend sandbox ${id}'s lease toward its timeout: ${error instanceof Error ? error.message : String(error)}. Trying again in five minutes.`, { code: "RUNTIME_E2B_LEASE" });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (keepers.get(id) === keeper)
|
|
116
|
+
schedule(keeper);
|
|
117
|
+
}
|
|
118
|
+
/** Test hook: renew every kept lease now, as the five-minute timer would. */
|
|
119
|
+
export async function renewLeases() {
|
|
120
|
+
await Promise.all([...keepers.values()].map((keeper) => renew(keeper)));
|
|
121
|
+
}
|
|
122
|
+
/** Test hook: the end each kept sandbox is kept until. */
|
|
123
|
+
export function keptLeases() {
|
|
124
|
+
return new Map([...keepers].map(([id, keeper]) => [id, keeper.until]));
|
|
125
|
+
}
|
|
36
126
|
function onLeaseEnd(lifecycle) {
|
|
37
127
|
if (!lifecycle)
|
|
38
128
|
return "stop";
|
|
@@ -114,7 +204,8 @@ function infoOf(runtime) {
|
|
|
114
204
|
...(info.name ? { name: info.name } : {}),
|
|
115
205
|
metadata: info.labels ?? {},
|
|
116
206
|
startedAt: new Date(info.createdAt),
|
|
117
|
-
|
|
207
|
+
// With no time limit, where it is paid up to: always ahead, moving on.
|
|
208
|
+
endAt: new Date(info.endsAt ?? info.expiresAt),
|
|
118
209
|
state: current === "stopped" ? "paused" : current,
|
|
119
210
|
cpuCount: info.vcpu,
|
|
120
211
|
memoryMB: info.memoryMiB,
|
|
@@ -215,8 +306,10 @@ export class Sandbox {
|
|
|
215
306
|
options.signal?.throwIfAborted();
|
|
216
307
|
refuseCreate(opts);
|
|
217
308
|
const client = clientFor(opts);
|
|
309
|
+
// No timeoutMs, no time limit: it runs while it works (0300).
|
|
310
|
+
const asked = opts.timeoutMs === undefined ? undefined : Date.now() + opts.timeoutMs;
|
|
218
311
|
const input = {
|
|
219
|
-
timeoutSeconds: leaseSeconds(opts.timeoutMs
|
|
312
|
+
...(opts.timeoutMs === undefined ? {} : { timeoutSeconds: leaseSeconds(opts.timeoutMs) }),
|
|
220
313
|
onLeaseEnd: onLeaseEnd(opts.lifecycle),
|
|
221
314
|
// With a lifecycle, E2B resumes a paused sandbox on traffic only when
|
|
222
315
|
// asked (autoResume); Runtime's automatic wake is the same thing (0093).
|
|
@@ -237,6 +330,8 @@ export class Sandbox {
|
|
|
237
330
|
...opts.runtime?.create,
|
|
238
331
|
};
|
|
239
332
|
const runtime = await guard("sandbox", () => client.sandboxes.create(create, options));
|
|
333
|
+
if (asked !== undefined)
|
|
334
|
+
keepUntil(runtime, asked);
|
|
240
335
|
return new this(runtime, client, opts.requestTimeoutMs);
|
|
241
336
|
}
|
|
242
337
|
/** Connects to a sandbox by id, waking it if it is paused, as E2B does. A
|
|
@@ -261,6 +356,7 @@ export class Sandbox {
|
|
|
261
356
|
/** Stops the sandbox. False when it was not found or had already ended. */
|
|
262
357
|
static async kill(sandboxId, opts = {}) {
|
|
263
358
|
const client = clientFor(opts);
|
|
359
|
+
stopKeeping(sandboxId);
|
|
264
360
|
try {
|
|
265
361
|
const runtime = await guard("sandbox", () => client.sandboxes.get(sandboxId, request(opts)));
|
|
266
362
|
return await stop(runtime, opts);
|
|
@@ -272,6 +368,7 @@ export class Sandbox {
|
|
|
272
368
|
}
|
|
273
369
|
}
|
|
274
370
|
async kill(opts = {}) {
|
|
371
|
+
stopKeeping(this.sandboxId);
|
|
275
372
|
try {
|
|
276
373
|
return await stop(this.#runtime, opts);
|
|
277
374
|
}
|
|
@@ -281,8 +378,10 @@ export class Sandbox {
|
|
|
281
378
|
throw error;
|
|
282
379
|
}
|
|
283
380
|
}
|
|
284
|
-
/** Sets the sandbox to end `timeoutMs` from now
|
|
285
|
-
*
|
|
381
|
+
/** Sets the sandbox to end `timeoutMs` from now, up to 24 hours; past an
|
|
382
|
+
* hour the lease is moved on while this process runs. Runtime cannot end a
|
|
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. */
|
|
286
385
|
static async setTimeout(sandboxId, timeoutMs, opts = {}) {
|
|
287
386
|
const client = clientFor(opts);
|
|
288
387
|
const runtime = await guard("sandbox", () => client.sandboxes.get(sandboxId, request(opts)));
|
|
@@ -330,6 +429,7 @@ export class Sandbox {
|
|
|
330
429
|
/** Copies of this sandbox, memory and all. While Runtime's forks are
|
|
331
430
|
* switched off this throws NotSupportedError with Runtime's own words. */
|
|
332
431
|
static async fork(sandboxId, opts = {}) {
|
|
432
|
+
checkForkCount(opts.count);
|
|
333
433
|
const { count: _count, timeoutMs: _timeoutMs, ...connection } = opts;
|
|
334
434
|
const source = await this.connect(sandboxId, connection);
|
|
335
435
|
return source.fork(opts);
|
|
@@ -339,6 +439,7 @@ export class Sandbox {
|
|
|
339
439
|
// A fork's lease is Runtime's to set; one shorter than asked could not be
|
|
340
440
|
// honoured after the forks exist, so this is refused before any are made.
|
|
341
441
|
throw new NotSupportedError("A timeout for forks (fork timeoutMs)", "Fork without it, then call setTimeout(ms) on each fork.");
|
|
442
|
+
checkForkCount(opts.count);
|
|
342
443
|
const copies = await guard("sandbox", () => this.#runtime.fork({ count: opts.count ?? 1, ...request(opts) }));
|
|
343
444
|
// Copies keep the source's environment on Runtime's side.
|
|
344
445
|
const Kind = this.constructor;
|
|
@@ -378,6 +479,10 @@ export class Sandbox {
|
|
|
378
479
|
getHost(port) {
|
|
379
480
|
if (!Number.isInteger(port) || port < 1 || port > 65_535)
|
|
380
481
|
throw new InvalidArgumentError(`port must be a whole number from 1 to 65535, not ${port}.`);
|
|
482
|
+
// A trial sandbox's ports are private: a host alone would answer 404 or
|
|
483
|
+
// 401 to everyone. Say so now, with the address that works.
|
|
484
|
+
if (this.#runtime.info.funding === "trial")
|
|
485
|
+
throw new PublicPreviewNotAllowedError(this.sandboxId, port);
|
|
381
486
|
void this.#share(port).catch((error) => {
|
|
382
487
|
if (typeof process !== "undefined" && process.emitWarning)
|
|
383
488
|
process.emitWarning(`Sharing port ${port} for getHost failed: ${error instanceof Error ? error.message : String(error)}. getPublicHost(${port}) says why.`, { code: "RUNTIME_E2B_GET_HOST" });
|
|
@@ -388,6 +493,8 @@ export class Sandbox {
|
|
|
388
493
|
* its host, `<port>-<id>.<domain>`, once the share has landed. Anyone with
|
|
389
494
|
* the address can reach it; a browser sees a one-time page naming Runtime. */
|
|
390
495
|
async getPublicHost(port) {
|
|
496
|
+
if (this.#runtime.info.funding === "trial")
|
|
497
|
+
throw new PublicPreviewNotAllowedError(this.sandboxId, port);
|
|
391
498
|
return this.#share(port);
|
|
392
499
|
}
|
|
393
500
|
#share(port) {
|
|
@@ -439,18 +546,24 @@ async function resume(runtime, opts) {
|
|
|
439
546
|
const current = state(runtime);
|
|
440
547
|
if (current === "stopped")
|
|
441
548
|
throw new SandboxNotFoundError(`Sandbox ${runtime.id} has ended.`);
|
|
549
|
+
const asked = opts.timeoutMs === undefined ? undefined : Date.now() + opts.timeoutMs;
|
|
442
550
|
if (current === "paused") {
|
|
443
551
|
await guard("sandbox", () => runtime.wake({
|
|
444
552
|
...(opts.timeoutMs === undefined ? {} : { timeoutSeconds: leaseSeconds(opts.timeoutMs) }),
|
|
445
553
|
...request(opts),
|
|
446
554
|
}));
|
|
447
|
-
return;
|
|
448
555
|
}
|
|
449
|
-
if (opts.timeoutMs !== undefined) {
|
|
450
|
-
|
|
556
|
+
else if (opts.timeoutMs !== undefined) {
|
|
557
|
+
checkTimeout(opts.timeoutMs);
|
|
558
|
+
// No time limit: no end to move (0300).
|
|
559
|
+
if (runtime.info.endsAt === null)
|
|
560
|
+
return;
|
|
561
|
+
const later = Math.min(asked, Date.now() + KEEP_AHEAD_MS) - Date.parse(runtime.info.expiresAt);
|
|
451
562
|
if (later > 1000)
|
|
452
563
|
await guard("sandbox", () => runtime.extend(Math.ceil(later / 1000), request(opts)));
|
|
453
564
|
}
|
|
565
|
+
if (asked !== undefined && asked > (keepers.get(runtime.id)?.until ?? 0))
|
|
566
|
+
keepUntil(runtime, asked);
|
|
454
567
|
}
|
|
455
568
|
async function stop(runtime, opts) {
|
|
456
569
|
if (state(runtime) === "stopped")
|
|
@@ -459,13 +572,18 @@ async function stop(runtime, opts) {
|
|
|
459
572
|
return true;
|
|
460
573
|
}
|
|
461
574
|
async function extendTo(runtime, timeoutMs, opts) {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
575
|
+
checkTimeout(timeoutMs);
|
|
576
|
+
// No time limit: no end to move (0300).
|
|
577
|
+
if (runtime.info.endsAt === null)
|
|
578
|
+
return;
|
|
579
|
+
const asked = Date.now() + timeoutMs;
|
|
580
|
+
const end = Date.parse(runtime.info.endsAt ?? runtime.info.expiresAt);
|
|
581
|
+
if (asked - end < -1000)
|
|
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.");
|
|
583
|
+
const later = Math.min(asked, Date.now() + KEEP_AHEAD_MS) - end;
|
|
467
584
|
if (later > 1000)
|
|
468
585
|
await guard("sandbox", () => runtime.extend(Math.ceil(later / 1000), request(opts)));
|
|
586
|
+
keepUntil(runtime, asked);
|
|
469
587
|
}
|
|
470
588
|
async function pause(runtime, opts) {
|
|
471
589
|
if (opts.keepMemory === false)
|
|
@@ -480,51 +598,109 @@ async function pause(runtime, opts) {
|
|
|
480
598
|
}
|
|
481
599
|
const RUNNING = ["starting", "running", "resuming"];
|
|
482
600
|
const PAUSED = ["pausing", "paused"];
|
|
483
|
-
/** E2B's paginator: `while (p.hasNext) items.push(...(await p.nextItems()))`.
|
|
601
|
+
/** E2B's paginator: `while (p.hasNext) items.push(...(await p.nextItems()))`.
|
|
602
|
+
* Runtime filters by state and metadata itself. A template, `startedAfter`,
|
|
603
|
+
* newest-first order or a saved `nextToken` are done here: every match is
|
|
604
|
+
* read once, then served a page at a time. */
|
|
484
605
|
export class SandboxPaginator {
|
|
485
606
|
#client;
|
|
486
607
|
#request;
|
|
487
608
|
#filter;
|
|
609
|
+
#opts;
|
|
610
|
+
#pageSize;
|
|
611
|
+
#local;
|
|
488
612
|
#page;
|
|
613
|
+
#all;
|
|
614
|
+
#offset;
|
|
489
615
|
#hasNext = true;
|
|
490
616
|
constructor(client, opts) {
|
|
491
617
|
this.#client = client;
|
|
492
618
|
this.#request = request(opts);
|
|
619
|
+
this.#opts = opts;
|
|
493
620
|
const query = opts.query ?? {};
|
|
494
|
-
if (
|
|
495
|
-
throw new
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
621
|
+
if (opts.limit !== undefined && (!Number.isSafeInteger(opts.limit) || opts.limit < 1))
|
|
622
|
+
throw new InvalidArgumentError(`limit must be a positive whole number, not ${opts.limit}.`);
|
|
623
|
+
this.#pageSize = opts.limit ?? 100;
|
|
624
|
+
this.#offset = opts.nextToken === undefined ? 0 : tokenOffset(opts.nextToken);
|
|
625
|
+
this.#local =
|
|
626
|
+
query.template !== undefined ||
|
|
627
|
+
query.startedAfter !== undefined ||
|
|
628
|
+
opts.order === "desc" ||
|
|
629
|
+
opts.nextToken !== undefined;
|
|
502
630
|
const states = query.state?.length ? query.state : ["running", "paused"];
|
|
503
631
|
this.#filter = {
|
|
504
632
|
state: states.flatMap((one) => (one === "running" ? [...RUNNING] : [...PAUSED])),
|
|
505
633
|
...(query.metadata && Object.keys(query.metadata).length ? { labels: query.metadata } : {}),
|
|
506
|
-
|
|
634
|
+
limit: this.#local ? 100 : Math.min(this.#pageSize, 100),
|
|
507
635
|
};
|
|
508
636
|
}
|
|
509
637
|
get hasNext() {
|
|
510
638
|
return this.#hasNext;
|
|
511
639
|
}
|
|
640
|
+
/** Where the next page starts; `Sandbox.list({ nextToken })` resumes there. */
|
|
512
641
|
get nextToken() {
|
|
513
|
-
return this.#
|
|
642
|
+
return this.#hasNext && this.#offset > 0 ? `runtime:${this.#offset}` : undefined;
|
|
514
643
|
}
|
|
515
644
|
async nextItems() {
|
|
516
645
|
this.#request.signal?.throwIfAborted();
|
|
517
646
|
if (!this.#hasNext)
|
|
518
647
|
throw new SandboxError("No more items to fetch.");
|
|
648
|
+
if (this.#local) {
|
|
649
|
+
const all = (this.#all ??= await guard("other", () => this.#everyMatch()));
|
|
650
|
+
const items = all.slice(this.#offset, this.#offset + this.#pageSize);
|
|
651
|
+
this.#offset += items.length;
|
|
652
|
+
this.#hasNext = this.#offset < all.length;
|
|
653
|
+
return items;
|
|
654
|
+
}
|
|
519
655
|
const previous = this.#page;
|
|
520
656
|
const page = await guard("other", async () => previous
|
|
521
657
|
? await previous.next()
|
|
522
658
|
: await this.#client.sandboxes.list(this.#filter, this.#request));
|
|
523
659
|
this.#page = page ?? undefined;
|
|
524
660
|
this.#hasNext = Boolean(page?.hasMore);
|
|
525
|
-
|
|
661
|
+
const items = (page?.data ?? []).map(infoOf);
|
|
662
|
+
this.#offset += items.length;
|
|
663
|
+
return items;
|
|
664
|
+
}
|
|
665
|
+
async #everyMatch() {
|
|
666
|
+
const query = this.#opts.query ?? {};
|
|
667
|
+
const templates = query.template === undefined
|
|
668
|
+
? undefined
|
|
669
|
+
: await templateIds(this.#client, query.template, this.#request);
|
|
670
|
+
const after = query.startedAfter?.getTime();
|
|
671
|
+
const all = [];
|
|
672
|
+
let page = await this.#client.sandboxes.list(this.#filter, this.#request);
|
|
673
|
+
while (page) {
|
|
674
|
+
for (const one of page.data) {
|
|
675
|
+
const info = infoOf(one);
|
|
676
|
+
if (templates && !templates.has(info.templateId))
|
|
677
|
+
continue;
|
|
678
|
+
if (after !== undefined && info.startedAt.getTime() < after)
|
|
679
|
+
continue;
|
|
680
|
+
all.push(info);
|
|
681
|
+
}
|
|
682
|
+
page = page.hasMore ? await page.next() : null;
|
|
683
|
+
}
|
|
684
|
+
const sign = this.#opts.order === "desc" ? -1 : 1;
|
|
685
|
+
return all.sort((a, b) => sign * (a.startedAt.getTime() - b.startedAt.getTime()));
|
|
526
686
|
}
|
|
527
687
|
}
|
|
688
|
+
function tokenOffset(token) {
|
|
689
|
+
const match = /^runtime:(\d+)$/.exec(token);
|
|
690
|
+
if (!match)
|
|
691
|
+
throw new InvalidArgumentError(`"${token}" is not a nextToken this package gave; pass the paginator's own nextToken.`);
|
|
692
|
+
return Number(match[1]);
|
|
693
|
+
}
|
|
694
|
+
/** The template ids a template name stands for: "base" for the stock names,
|
|
695
|
+
* the id itself for a UUID, and every Runtime image of that name. */
|
|
696
|
+
async function templateIds(client, template, options) {
|
|
697
|
+
if (STOCK_TEMPLATES.has(template))
|
|
698
|
+
return new Set(["base"]);
|
|
699
|
+
if (UUID.test(template))
|
|
700
|
+
return new Set([template]);
|
|
701
|
+
const page = await client.images.list({ name: template, limit: 100 }, options);
|
|
702
|
+
return new Set(page.data.map((image) => image.id));
|
|
703
|
+
}
|
|
528
704
|
async function e2bMetrics(runtime, opts) {
|
|
529
705
|
const start = opts.start === undefined ? Date.now() - 15 * 60_000 : new Date(opts.start).getTime();
|
|
530
706
|
const end = opts.end === undefined ? Date.now() : new Date(opts.end).getTime();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { FileEntry } from "../types.js";
|
|
2
|
+
import type { RequestOptions } from "../transport.js";
|
|
3
|
+
import type { SandboxContext, Username } from "./commands.js";
|
|
4
|
+
/** The Linux user a call runs as, or undefined for the sandbox's own. Refuses
|
|
5
|
+
* a user the sandbox does not have, once per user and sandbox. */
|
|
6
|
+
export declare function runAs(ctx: SandboxContext, user: Username | undefined, options: RequestOptions): Promise<string | undefined>;
|
|
7
|
+
/** `bash -c script` as `user`, keeping the environment Runtime gives the
|
|
8
|
+
* command. Without a `cwd` it starts in the user's home, as E2B's does. */
|
|
9
|
+
export declare function commandAs(user: string, script: string, cwdGiven: boolean): string[];
|
|
10
|
+
/** An interactive login shell as `user`, for a PTY. */
|
|
11
|
+
export declare function shellAs(user: string, cwdGiven: boolean): string[];
|
|
12
|
+
/** E2B's file calls as another Linux user: each one a short script run with
|
|
13
|
+
* `sudo -u`, since Runtime's file API acts as the sandbox's own user. Output
|
|
14
|
+
* streams, so a read of any size comes back whole. */
|
|
15
|
+
export declare class FilesAs {
|
|
16
|
+
#private;
|
|
17
|
+
private readonly ctx;
|
|
18
|
+
private readonly user;
|
|
19
|
+
private readonly options;
|
|
20
|
+
constructor(ctx: SandboxContext, user: string, options: RequestOptions);
|
|
21
|
+
read(path: string): Promise<Uint8Array>;
|
|
22
|
+
write(path: string, bytes: Uint8Array): Promise<void>;
|
|
23
|
+
list(path: string, depth: number): Promise<FileEntry[]>;
|
|
24
|
+
stat(path: string): Promise<FileEntry>;
|
|
25
|
+
exists(path: string): Promise<boolean>;
|
|
26
|
+
/** False when it was there already. */
|
|
27
|
+
makeDir(path: string): Promise<boolean>;
|
|
28
|
+
rename(from: string, to: string): Promise<void>;
|
|
29
|
+
remove(path: string): Promise<void>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { FileNotFoundError, guard, InvalidArgumentError, SandboxError } from "./errors.js";
|
|
2
|
+
/* E2B's `user` option, on Runtime. A sandbox's own user is `runtime`, with
|
|
3
|
+
passwordless sudo; E2B's default user is called "user". So "user" (or no
|
|
4
|
+
user) runs as the sandbox's own user, and any other name runs through
|
|
5
|
+
`sudo -u <name>`. The user must already exist: the adapter never makes one,
|
|
6
|
+
and a name the sandbox does not know is refused before anything runs. */
|
|
7
|
+
const NAME = /^[a-z_][a-z0-9_.-]{0,31}$/i;
|
|
8
|
+
/** Exit codes the scripts below use to say what went wrong. */
|
|
9
|
+
const MISSING = 44;
|
|
10
|
+
const NOT_DIRECTORY = 45;
|
|
11
|
+
const IS_DIRECTORY = 46;
|
|
12
|
+
const EXISTS = 47;
|
|
13
|
+
/** Base64 text per exec: under the 1 MiB an exec's input may carry. */
|
|
14
|
+
const WRITE_CHUNK = 786_432;
|
|
15
|
+
/** Only the caller's signal: an exec's own `timeoutMs` is the command's. */
|
|
16
|
+
const signalOf = (options) => (options.signal ? { signal: options.signal } : {});
|
|
17
|
+
const checked = new WeakMap();
|
|
18
|
+
/** The Linux user a call runs as, or undefined for the sandbox's own. Refuses
|
|
19
|
+
* a user the sandbox does not have, once per user and sandbox. */
|
|
20
|
+
export async function runAs(ctx, user, options) {
|
|
21
|
+
if (user === undefined || user === "user")
|
|
22
|
+
return undefined;
|
|
23
|
+
if (typeof user !== "string" || !NAME.test(user))
|
|
24
|
+
throw new InvalidArgumentError(`"${String(user)}" is not a Linux user name.`);
|
|
25
|
+
if (user === "root")
|
|
26
|
+
return user;
|
|
27
|
+
let users = checked.get(ctx);
|
|
28
|
+
if (!users)
|
|
29
|
+
checked.set(ctx, (users = new Map()));
|
|
30
|
+
let check = users.get(user);
|
|
31
|
+
if (!check) {
|
|
32
|
+
check = (async () => {
|
|
33
|
+
const found = await guard("sandbox", () => ctx.runtime.exec(["id", "-u", "--", user], signalOf(options)));
|
|
34
|
+
if (found.exitCode !== 0)
|
|
35
|
+
throw new InvalidArgumentError(`This sandbox has no user "${user}", and Runtime never creates one for you. ` +
|
|
36
|
+
`Make it first, as root: sandbox.commands.run("useradd -m ${user}", { user: "root" }). ` +
|
|
37
|
+
`"user" is the sandbox's own user and "root" is root.`);
|
|
38
|
+
})();
|
|
39
|
+
users.set(user, check);
|
|
40
|
+
check.catch(() => users.delete(user));
|
|
41
|
+
}
|
|
42
|
+
await check;
|
|
43
|
+
return user;
|
|
44
|
+
}
|
|
45
|
+
/** `bash -c script` as `user`, keeping the environment Runtime gives the
|
|
46
|
+
* command. Without a `cwd` it starts in the user's home, as E2B's does. */
|
|
47
|
+
export function commandAs(user, script, cwdGiven) {
|
|
48
|
+
return [
|
|
49
|
+
"sudo",
|
|
50
|
+
"-n",
|
|
51
|
+
"-E",
|
|
52
|
+
"-H",
|
|
53
|
+
"-u",
|
|
54
|
+
user,
|
|
55
|
+
"--",
|
|
56
|
+
"/bin/bash",
|
|
57
|
+
"-c",
|
|
58
|
+
cwdGiven ? script : `cd ~ 2>/dev/null\n${script}`,
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
/** An interactive login shell as `user`, for a PTY. */
|
|
62
|
+
export function shellAs(user, cwdGiven) {
|
|
63
|
+
return commandAs(user, "exec /bin/bash -i -l", cwdGiven);
|
|
64
|
+
}
|
|
65
|
+
/** One `sh -c` script as `user`, with its arguments passed apart, never
|
|
66
|
+
* pasted into the script. */
|
|
67
|
+
function script(user, text, ...args) {
|
|
68
|
+
return ["sudo", "-n", "-u", user, "--", "/bin/sh", "-c", text, "sh", ...args];
|
|
69
|
+
}
|
|
70
|
+
/** Each entry `find -printf` prints: eight fields, each ended by NUL. */
|
|
71
|
+
const FIELDS = "%y\\0%s\\0%m\\0%u\\0%g\\0%T@\\0%p\\0%l\\0";
|
|
72
|
+
const TYPES = { f: "file", d: "directory", l: "symlink" };
|
|
73
|
+
function entries(output) {
|
|
74
|
+
const fields = output.split("\0");
|
|
75
|
+
const out = [];
|
|
76
|
+
for (let i = 0; i + 8 <= fields.length; i += 8) {
|
|
77
|
+
const [kind, size, mode, owner, group, mtime, path, link] = fields.slice(i, i + 8);
|
|
78
|
+
out.push({
|
|
79
|
+
name: path.split("/").filter(Boolean).pop() ?? path,
|
|
80
|
+
path,
|
|
81
|
+
type: TYPES[kind] ?? "other",
|
|
82
|
+
size: Number(size) || 0,
|
|
83
|
+
mode: `0${mode}`,
|
|
84
|
+
modifiedAt: new Date(Math.round(Number(mtime) * 1000)).toISOString(),
|
|
85
|
+
owner,
|
|
86
|
+
group,
|
|
87
|
+
...(kind === "l" && link ? { symlinkTarget: link } : {}),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
/** E2B's file calls as another Linux user: each one a short script run with
|
|
93
|
+
* `sudo -u`, since Runtime's file API acts as the sandbox's own user. Output
|
|
94
|
+
* streams, so a read of any size comes back whole. */
|
|
95
|
+
export class FilesAs {
|
|
96
|
+
ctx;
|
|
97
|
+
user;
|
|
98
|
+
options;
|
|
99
|
+
constructor(ctx, user, options) {
|
|
100
|
+
this.ctx = ctx;
|
|
101
|
+
this.user = user;
|
|
102
|
+
this.options = options;
|
|
103
|
+
}
|
|
104
|
+
async #run(argv, stdin) {
|
|
105
|
+
const result = await guard("sandbox", () => this.ctx.runtime.exec(argv, {
|
|
106
|
+
...(stdin === undefined ? {} : { stdin }),
|
|
107
|
+
// A callback makes the exec stream, so its output is never cut.
|
|
108
|
+
onStdout: () => undefined,
|
|
109
|
+
...signalOf(this.options),
|
|
110
|
+
}));
|
|
111
|
+
if (result.stdoutTruncated)
|
|
112
|
+
throw new SandboxError("Part of the answer was lost on the way; try again.");
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
#fail(path, result) {
|
|
116
|
+
if (result.exitCode === MISSING)
|
|
117
|
+
throw new FileNotFoundError(`${path} does not exist.`);
|
|
118
|
+
if (result.exitCode === NOT_DIRECTORY)
|
|
119
|
+
throw new InvalidArgumentError(`${path} is not a directory.`);
|
|
120
|
+
if (result.exitCode === IS_DIRECTORY)
|
|
121
|
+
throw new InvalidArgumentError(`${path} is a directory.`);
|
|
122
|
+
throw new SandboxError(`As the user "${this.user}": ${result.stderr.trim() || `exit status ${result.exitCode}`}`);
|
|
123
|
+
}
|
|
124
|
+
async read(path) {
|
|
125
|
+
const result = await this.#run(script(this.user, `[ -e "$1" ] || exit ${MISSING}; [ -d "$1" ] && exit ${IS_DIRECTORY}; exec base64 -w 0 -- "$1"`, path));
|
|
126
|
+
if (result.exitCode !== 0)
|
|
127
|
+
this.#fail(path, result);
|
|
128
|
+
return Uint8Array.from(atob(result.stdout.trim()), (c) => c.charCodeAt(0));
|
|
129
|
+
}
|
|
130
|
+
async write(path, bytes) {
|
|
131
|
+
let text = "";
|
|
132
|
+
for (let i = 0; i < bytes.length; i += 32_768)
|
|
133
|
+
text += String.fromCharCode(...bytes.subarray(i, i + 32_768));
|
|
134
|
+
const encoded = btoa(text);
|
|
135
|
+
let offset = 0;
|
|
136
|
+
do {
|
|
137
|
+
const chunk = encoded.slice(offset, offset + WRITE_CHUNK);
|
|
138
|
+
const result = await this.#run(script(this.user, offset === 0
|
|
139
|
+
? `mkdir -p -- "$(dirname -- "$1")" && base64 -d > "$1"`
|
|
140
|
+
: `base64 -d >> "$1"`, path), chunk);
|
|
141
|
+
if (result.exitCode !== 0)
|
|
142
|
+
this.#fail(path, result);
|
|
143
|
+
offset += WRITE_CHUNK;
|
|
144
|
+
} while (offset < encoded.length);
|
|
145
|
+
}
|
|
146
|
+
async list(path, depth) {
|
|
147
|
+
const result = await this.#run(script(this.user, `[ -e "$1" ] || exit ${MISSING}; [ -d "$1" ] || exit ${NOT_DIRECTORY}; ` +
|
|
148
|
+
`exec find "$1" -mindepth 1 -maxdepth "$2" -printf '${FIELDS}'`, path, String(depth)));
|
|
149
|
+
if (result.exitCode !== 0)
|
|
150
|
+
this.#fail(path, result);
|
|
151
|
+
return entries(result.stdout);
|
|
152
|
+
}
|
|
153
|
+
async stat(path) {
|
|
154
|
+
const result = await this.#run(script(this.user, `[ -e "$1" ] || [ -L "$1" ] || exit ${MISSING}; exec find "$1" -maxdepth 0 -printf '${FIELDS}'`, path));
|
|
155
|
+
if (result.exitCode !== 0)
|
|
156
|
+
this.#fail(path, result);
|
|
157
|
+
const [entry] = entries(result.stdout);
|
|
158
|
+
if (!entry)
|
|
159
|
+
throw new FileNotFoundError(`${path} does not exist.`);
|
|
160
|
+
return entry;
|
|
161
|
+
}
|
|
162
|
+
async exists(path) {
|
|
163
|
+
const result = await this.#run(script(this.user, `[ -e "$1" ] || [ -L "$1" ]`, path));
|
|
164
|
+
return result.exitCode === 0;
|
|
165
|
+
}
|
|
166
|
+
/** False when it was there already. */
|
|
167
|
+
async makeDir(path) {
|
|
168
|
+
const result = await this.#run(script(this.user, `[ -d "$1" ] && exit ${EXISTS}; mkdir -p -- "$1"`, path));
|
|
169
|
+
if (result.exitCode === EXISTS)
|
|
170
|
+
return false;
|
|
171
|
+
if (result.exitCode !== 0)
|
|
172
|
+
this.#fail(path, result);
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
async rename(from, to) {
|
|
176
|
+
const result = await this.#run(script(this.user, `[ -e "$1" ] || [ -L "$1" ] || exit ${MISSING}; mv -fT -- "$1" "$2"`, from, to));
|
|
177
|
+
if (result.exitCode !== 0)
|
|
178
|
+
this.#fail(from, result);
|
|
179
|
+
}
|
|
180
|
+
async remove(path) {
|
|
181
|
+
const result = await this.#run(script(this.user, `rm -rf -- "$1"`, path));
|
|
182
|
+
if (result.exitCode !== 0)
|
|
183
|
+
this.#fail(path, result);
|
|
184
|
+
}
|
|
185
|
+
}
|
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
|
}
|