withruntime 0.12.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BLAXEL.md +97 -32
- package/COMPATIBILITY.md +120 -5
- package/DAYTONA.md +34 -3
- package/E2B.md +34 -5
- package/README.md +36 -3
- package/README.src.md +35 -2
- package/VERCEL.md +52 -5
- package/compatibility-lock.json +71 -111
- package/dist/billing-cli.js +8 -4
- package/dist/blaxel/client.d.ts +11 -0
- package/dist/blaxel/client.js +28 -0
- package/dist/blaxel/errors.d.ts +65 -0
- package/dist/blaxel/errors.js +114 -0
- package/dist/blaxel/index.d.ts +10 -3
- package/dist/blaxel/index.js +23 -4
- package/dist/blaxel/preview.d.ts +29 -4
- package/dist/blaxel/preview.js +79 -5
- package/dist/blaxel/sandbox.d.ts +1 -1
- package/dist/blaxel/sandbox.js +2 -1
- package/dist/blaxel/session.js +4 -2
- package/dist/cli-extras.js +2 -1
- package/dist/cli-words.js +1 -0
- package/dist/cli.d.ts +17 -2
- package/dist/cli.js +291 -85
- package/dist/client.d.ts +1 -1
- package/dist/credentials.d.ts +3 -0
- package/dist/credentials.js +15 -0
- package/dist/daytona/client.d.ts +4 -0
- package/dist/daytona/client.js +8 -3
- package/dist/daytona/daytona.d.ts +4 -1
- package/dist/daytona/daytona.js +3 -2
- package/dist/e2b/sandbox.d.ts +7 -6
- package/dist/e2b/sandbox.js +9 -12
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -1
- package/dist/freestyle/index.d.ts +77 -5
- package/dist/freestyle/index.js +380 -48
- package/dist/identity.js +36 -18
- package/dist/index.d.ts +4 -3
- package/dist/machine.d.ts +39 -13
- package/dist/machine.js +72 -25
- package/dist/mcp.js +157 -20
- package/dist/modal/index.d.ts +15 -0
- package/dist/modal/index.js +35 -7
- package/dist/morph/index.d.ts +255 -0
- package/dist/morph/index.js +641 -0
- package/dist/network-cli.js +2 -2
- package/dist/product-names.d.ts +2 -2
- package/dist/product-names.js +2 -2
- package/dist/products/api-keys.d.ts +73 -0
- package/dist/products/api-keys.js +23 -0
- package/dist/products/apps.d.ts +12 -31
- package/dist/products/apps.js +8 -113
- package/dist/products/balancers.d.ts +87 -0
- package/dist/products/balancers.js +8 -0
- package/dist/products/builds.d.ts +30 -0
- package/dist/products/builds.js +6 -0
- package/dist/products/index.d.ts +2 -0
- package/dist/products/index.js +2 -0
- package/dist/products/jobs.d.ts +2 -2
- package/dist/products/jobs.js +1 -1
- package/dist/products/limits.d.ts +16 -2
- package/dist/products/limits.js +2 -2
- package/dist/products/mcp.d.ts +5 -0
- package/dist/products/network-products.d.ts +21 -3
- package/dist/products/network-products.js +3 -1
- package/dist/products/observability.d.ts +7 -2
- package/dist/products/previews.d.ts +6 -3
- package/dist/products/previews.js +1 -1
- package/dist/products/resources.d.ts +54 -0
- package/dist/products/resources.js +122 -0
- package/dist/products/secrets.d.ts +22 -0
- package/dist/products/secrets.js +8 -0
- package/dist/products/volumes.d.ts +3 -0
- package/dist/sandbox.d.ts +4 -0
- package/dist/sandbox.js +27 -2
- package/dist/snapshots.d.ts +4 -0
- package/dist/ssh.d.ts +35 -0
- package/dist/ssh.js +93 -1
- package/dist/transport.d.ts +14 -8
- package/dist/transport.js +70 -15
- package/dist/types.d.ts +33 -5
- package/dist/vercel/command.d.ts +48 -1
- package/dist/vercel/command.js +237 -6
- package/dist/vercel/sandbox-user.js +2 -1
- package/dist/vercel/sandbox.d.ts +17 -3
- package/dist/vercel/sandbox.js +46 -3
- package/dist/wallet.d.ts +4 -1
- package/dist/wallet.js +2 -1
- package/package.json +7 -14
- package/dist/cli/logs.test.d.ts +0 -1
- package/dist/cli/logs.test.js +0 -272
- package/dist/products/logs.test.d.ts +0 -1
- package/dist/products/logs.test.js +0 -343
package/dist/machine.d.ts
CHANGED
|
@@ -57,18 +57,21 @@ export declare abstract class Machine<Info extends MachineInfo, Settings extends
|
|
|
57
57
|
* answers the same. */
|
|
58
58
|
delete(options?: RequestOptions): Promise<Deleted>;
|
|
59
59
|
/** Carries on a paused sandbox; for one with a time limit,
|
|
60
|
-
* `timeoutSeconds` is its new one.
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
* `timeoutSeconds` is its new one. A wake stays on its server; when that
|
|
61
|
+
* server is full for now (`no_capacity`) it waits for room as `create`
|
|
62
|
+
* does, up to `waitForCapacityMs` (the client's, two minutes by default). */
|
|
63
|
+
wake(options?: LifecycleOptions & {
|
|
63
64
|
timeoutSeconds?: number;
|
|
64
65
|
}): Promise<this>;
|
|
65
|
-
/** More time before its time limit ends (at most
|
|
66
|
+
/** More time before its time limit ends (at most 24 hours ahead of now). A
|
|
66
67
|
* sandbox with no time limit needs none: the call answers at once and
|
|
67
|
-
* changes nothing.
|
|
68
|
+
* changes nothing. To set the end sooner, or give one with no limit a limit,
|
|
69
|
+
* `update({ timeoutSeconds })`. */
|
|
68
70
|
extend(seconds: number, options?: RequestOptions): Promise<this>;
|
|
69
|
-
/** Changes its name, labels, automatic wake, idle pause
|
|
70
|
-
* fields left out stay as they are. `persistent:
|
|
71
|
-
* while credit lasts (paid only).
|
|
71
|
+
/** Changes its name, labels, automatic wake, idle pause, time limit, end
|
|
72
|
+
* action or persistence; fields left out stay as they are. `persistent:
|
|
73
|
+
* true` keeps it running while credit lasts (paid only). `timeoutSeconds`
|
|
74
|
+
* ends it that long from now, sooner or later than before (0: no limit). */
|
|
72
75
|
update(settings: Settings, options?: RequestOptions): Promise<this>;
|
|
73
76
|
/** Moves it to another image (id, name, name:tag or name@version), keeping
|
|
74
77
|
* its id, /workspace (its home: dotfiles, pip --user and npm -g installs),
|
|
@@ -116,10 +119,10 @@ export declare abstract class Machine<Info extends MachineInfo, Settings extends
|
|
|
116
119
|
* `timeoutMs` (ten minutes unless given) mid-capture, it stays paused and
|
|
117
120
|
* the error names the snapshot. */
|
|
118
121
|
snapshot(options?: SnapshotOptions & RequestOptions): Promise<Snapshot>;
|
|
119
|
-
/** Starts a stopped persistent sandbox again from its disk (memory is not
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
/** Starts a stopped persistent sandbox again from its disk (memory is not
|
|
123
|
+
* kept), waiting for room on a full server as `wake` does. Its image's
|
|
124
|
+
* start command runs again, reported in `info.start`. */
|
|
125
|
+
restart(options?: LifecycleOptions): Promise<this>;
|
|
123
126
|
/** The end of an `await using` block. A sandbox this client created is
|
|
124
127
|
* deleted, as E2B's block kills it, unless it was created with
|
|
125
128
|
* `keep: true`; one it found with `get` or `getOrCreate` is stopped, which
|
|
@@ -137,6 +140,15 @@ export type CreateOptions = RequestOptions & {
|
|
|
137
140
|
* next try: to tell a person why nothing has happened yet. */
|
|
138
141
|
onCapacityWait?: (refusal: RuntimeError, waitMs: number) => void;
|
|
139
142
|
};
|
|
143
|
+
/** How `wake` and `restart` wait when the sandbox's server is full for now. */
|
|
144
|
+
export type LifecycleOptions = RequestOptions & {
|
|
145
|
+
wait?: boolean;
|
|
146
|
+
/** How long to keep retrying `no_capacity` and the like. Default: the
|
|
147
|
+
* client's, two minutes; 0 fails at once. */
|
|
148
|
+
waitForCapacityMs?: number;
|
|
149
|
+
/** Called before each wait, with the refusal and the milliseconds until the next try. */
|
|
150
|
+
onCapacityWait?: (refusal: RuntimeError, waitMs: number) => void;
|
|
151
|
+
};
|
|
140
152
|
/** A machine product's collection: `runtime.<product>`. */
|
|
141
153
|
export declare abstract class Machines<M extends Machine<Info, object, Deleted>, Info extends MachineInfo, Create extends {
|
|
142
154
|
name?: string;
|
|
@@ -146,7 +158,7 @@ export declare abstract class Machines<M extends Machine<Info, object, Deleted>,
|
|
|
146
158
|
#private;
|
|
147
159
|
constructor(product: MachineProduct, t: Transport, wrap: (t: Transport, info: Info) => M);
|
|
148
160
|
/** Creates a sandbox and waits until it is running. Every field is optional:
|
|
149
|
-
* with none you get the
|
|
161
|
+
* with none you get the free usage while it lasts, the default region and a
|
|
150
162
|
* 2 vCPU / 4 GiB machine from a warm template. `wait: false` returns at once.
|
|
151
163
|
* When every slot without credit or the account's quota is taken, it waits for one to
|
|
152
164
|
* free, up to `waitForCapacityMs` (the client's, two minutes by default). */
|
|
@@ -186,4 +198,18 @@ export declare abstract class Machines<M extends Machine<Info, object, Deleted>,
|
|
|
186
198
|
error: unknown;
|
|
187
199
|
}[];
|
|
188
200
|
}>;
|
|
201
|
+
/** Deletes for good every sandbox whose labels all match, stopped ones
|
|
202
|
+
* included, eight at a time, as `delete()` does one: its disk and paused
|
|
203
|
+
* memory go, its snapshots stay. Says which were deleted and which failed;
|
|
204
|
+
* one failure does not stop the rest. Needs at least one label, so deleting
|
|
205
|
+
* everything is never one call. */
|
|
206
|
+
deleteAll(filter: {
|
|
207
|
+
labels: Record<string, string>;
|
|
208
|
+
}, options?: RequestOptions): Promise<{
|
|
209
|
+
deleted: string[];
|
|
210
|
+
failed: {
|
|
211
|
+
id: string;
|
|
212
|
+
error: unknown;
|
|
213
|
+
}[];
|
|
214
|
+
}>;
|
|
189
215
|
}
|
package/dist/machine.js
CHANGED
|
@@ -41,7 +41,7 @@ export function machinePage(product, t, wrap, body, query, options = {}) {
|
|
|
41
41
|
}
|
|
42
42
|
/** One machine: its record and the verbs every machine product shares. */
|
|
43
43
|
/** Machines `create` made, which the end of an `await using` block deletes
|
|
44
|
-
* (
|
|
44
|
+
* (6 October 2026: deleted unless the code passes `keep: true`). One
|
|
45
45
|
* from `get`, or an existing one `getOrCreate` found, is only stopped. */
|
|
46
46
|
const deleteAtBlockEnd = new WeakSet();
|
|
47
47
|
export class Machine {
|
|
@@ -111,7 +111,9 @@ export class Machine {
|
|
|
111
111
|
return deleteMachine(this.#product, this.#t, this.id, options);
|
|
112
112
|
}
|
|
113
113
|
/** Carries on a paused sandbox; for one with a time limit,
|
|
114
|
-
* `timeoutSeconds` is its new one.
|
|
114
|
+
* `timeoutSeconds` is its new one. A wake stays on its server; when that
|
|
115
|
+
* server is full for now (`no_capacity`) it waits for room as `create`
|
|
116
|
+
* does, up to `waitForCapacityMs` (the client's, two minutes by default). */
|
|
115
117
|
async wake(options = {}) {
|
|
116
118
|
const { timeoutSeconds, ...rest } = options;
|
|
117
119
|
try {
|
|
@@ -128,15 +130,17 @@ export class Machine {
|
|
|
128
130
|
return this;
|
|
129
131
|
}
|
|
130
132
|
}
|
|
131
|
-
/** More time before its time limit ends (at most
|
|
133
|
+
/** More time before its time limit ends (at most 24 hours ahead of now). A
|
|
132
134
|
* sandbox with no time limit needs none: the call answers at once and
|
|
133
|
-
* changes nothing.
|
|
135
|
+
* changes nothing. To set the end sooner, or give one with no limit a limit,
|
|
136
|
+
* `update({ timeoutSeconds })`. */
|
|
134
137
|
async extend(seconds, options = {}) {
|
|
135
138
|
return this.#lifecycle("extend", { ...options, wait: false }, { seconds });
|
|
136
139
|
}
|
|
137
|
-
/** Changes its name, labels, automatic wake, idle pause
|
|
138
|
-
* fields left out stay as they are. `persistent:
|
|
139
|
-
* while credit lasts (paid only).
|
|
140
|
+
/** Changes its name, labels, automatic wake, idle pause, time limit, end
|
|
141
|
+
* action or persistence; fields left out stay as they are. `persistent:
|
|
142
|
+
* true` keeps it running while credit lasts (paid only). `timeoutSeconds`
|
|
143
|
+
* ends it that long from now, sooner or later than before (0: no limit). */
|
|
140
144
|
async update(settings, options = {}) {
|
|
141
145
|
return this.#lifecycle("update", { ...options, wait: false }, { ...settings });
|
|
142
146
|
}
|
|
@@ -277,20 +281,26 @@ export class Machine {
|
|
|
277
281
|
await this.refresh(request());
|
|
278
282
|
// Straight after a fork or a wake the sandbox is still `resuming`, and
|
|
279
283
|
// after a pause still `pausing`: wait for where it is going, or a
|
|
280
|
-
// snapshot of it is refused as not paused (
|
|
284
|
+
// snapshot of it is refused as not paused (23 September 2026).
|
|
281
285
|
if (this.state === "resuming" || this.state === "starting")
|
|
282
286
|
await this.waitFor("running", request());
|
|
283
287
|
else if (this.state === "pausing")
|
|
284
288
|
await this.waitFor("paused", request());
|
|
285
289
|
running = this.state === "running";
|
|
286
290
|
// Check before entering cleanup: an abort during refresh must not pause or wake.
|
|
287
|
-
|
|
291
|
+
request();
|
|
288
292
|
let captured;
|
|
289
293
|
let primary;
|
|
290
294
|
let failed = false;
|
|
295
|
+
/** Whether the server may have left a running source paused: it answered
|
|
296
|
+
* before the capture was over, or not at all. */
|
|
297
|
+
let leftPaused = running;
|
|
291
298
|
try {
|
|
292
|
-
|
|
293
|
-
|
|
299
|
+
// One call: the server pauses a running source for the capture, and
|
|
300
|
+
// wakes it before it answers a capture that is over (ready or failed).
|
|
301
|
+
// Pausing, snapshotting and waking it from here took four calls: a
|
|
302
|
+
// repeat snapshot of a 4 GiB sandbox took 1.96 s at the median against
|
|
303
|
+
// 1.19 s for this one (15 October 2026).
|
|
294
304
|
let snapshot = await this.#t.json({
|
|
295
305
|
method: "POST",
|
|
296
306
|
path: this.#path(":snapshot"),
|
|
@@ -300,6 +310,7 @@ export class Machine {
|
|
|
300
310
|
...request(),
|
|
301
311
|
});
|
|
302
312
|
captured = snapshot;
|
|
313
|
+
leftPaused = running && snapshot.state === "capturing";
|
|
303
314
|
// Prefer: wait is bounded on the server. A capture still in progress
|
|
304
315
|
// must keep its source paused, or the worker refuses it after we wake.
|
|
305
316
|
while (snapshot.state === "capturing") {
|
|
@@ -337,9 +348,17 @@ export class Machine {
|
|
|
337
348
|
}
|
|
338
349
|
// A capture still running needs its source paused: the worker fails one
|
|
339
350
|
// whose source woke. The timeout says so and leaves the wake to the caller.
|
|
340
|
-
if (
|
|
351
|
+
if (leftPaused && !late) {
|
|
341
352
|
try {
|
|
342
|
-
|
|
353
|
+
// A call that failed may have been refused before anything paused
|
|
354
|
+
// (pausable: false, volumes): only a source that is paused is woken.
|
|
355
|
+
if (failed && captured === undefined)
|
|
356
|
+
await this.refresh();
|
|
357
|
+
if (!failed ||
|
|
358
|
+
captured !== undefined ||
|
|
359
|
+
this.state === "paused" ||
|
|
360
|
+
this.state === "pausing")
|
|
361
|
+
await this.wake();
|
|
343
362
|
}
|
|
344
363
|
catch (wakeError) {
|
|
345
364
|
const recovery = {
|
|
@@ -365,17 +384,30 @@ export class Machine {
|
|
|
365
384
|
throw primary;
|
|
366
385
|
return captured;
|
|
367
386
|
}
|
|
368
|
-
/** Starts a stopped persistent sandbox again from its disk (memory is not
|
|
387
|
+
/** Starts a stopped persistent sandbox again from its disk (memory is not
|
|
388
|
+
* kept), waiting for room on a full server as `wake` does. Its image's
|
|
389
|
+
* start command runs again, reported in `info.start`. */
|
|
369
390
|
async restart(options = {}) {
|
|
370
391
|
return this.#lifecycle("restart", options);
|
|
371
392
|
}
|
|
372
393
|
async #lifecycle(verb, options, body = {}) {
|
|
394
|
+
// A wake or restart refused for room ran nothing and is sent again with
|
|
395
|
+
// the same key (api.md, "A refusal is not kept"). Until 17 October 2026
|
|
396
|
+
// only create waited: the product check's wake was refused no_capacity
|
|
397
|
+
// in 36 ms on 16 October, where a create would have waited for room.
|
|
398
|
+
const room = verb === "wake" || verb === "restart"
|
|
399
|
+
? {
|
|
400
|
+
waitForCapacityMs: options.waitForCapacityMs ?? this.#t.waitForCapacityMs,
|
|
401
|
+
...(options.onCapacityWait ? { onCapacityWait: options.onCapacityWait } : {}),
|
|
402
|
+
}
|
|
403
|
+
: {};
|
|
373
404
|
this.#keep(await this.#t.json({
|
|
374
405
|
method: "POST",
|
|
375
406
|
path: this.#path(`:${verb}`),
|
|
376
407
|
body,
|
|
377
408
|
wait: options.wait === false ? 0 : 60,
|
|
378
409
|
...requestOnly(options),
|
|
410
|
+
...room,
|
|
379
411
|
}));
|
|
380
412
|
return this;
|
|
381
413
|
}
|
|
@@ -408,7 +440,7 @@ export class Machines {
|
|
|
408
440
|
this.#wrap = wrap;
|
|
409
441
|
}
|
|
410
442
|
/** Creates a sandbox and waits until it is running. Every field is optional:
|
|
411
|
-
* with none you get the
|
|
443
|
+
* with none you get the free usage while it lasts, the default region and a
|
|
412
444
|
* 2 vCPU / 4 GiB machine from a warm template. `wait: false` returns at once.
|
|
413
445
|
* When every slot without credit or the account's quota is taken, it waits for one to
|
|
414
446
|
* free, up to `waitForCapacityMs` (the client's, two minutes by default). */
|
|
@@ -493,29 +525,44 @@ export class Machines {
|
|
|
493
525
|
* says which stopped and which failed; one failure does not stop the rest.
|
|
494
526
|
* Needs at least one label, so stopping everything is never one call. */
|
|
495
527
|
async stopAll(filter, options = {}) {
|
|
528
|
+
const { done, failed } = await this.#eachLabelled("stop", filter, false, options, (m, each) => m.stop(each));
|
|
529
|
+
return { stopped: done, failed };
|
|
530
|
+
}
|
|
531
|
+
/** Deletes for good every sandbox whose labels all match, stopped ones
|
|
532
|
+
* included, eight at a time, as `delete()` does one: its disk and paused
|
|
533
|
+
* memory go, its snapshots stay. Says which were deleted and which failed;
|
|
534
|
+
* one failure does not stop the rest. Needs at least one label, so deleting
|
|
535
|
+
* everything is never one call. */
|
|
536
|
+
async deleteAll(filter, options = {}) {
|
|
537
|
+
const { done, failed } = await this.#eachLabelled("delete", filter, true, options, (m, each) => m.delete(each));
|
|
538
|
+
return { deleted: done, failed };
|
|
539
|
+
}
|
|
540
|
+
/** Lists every match first, then acts on each, eight at a time: acting
|
|
541
|
+
* while paging would move the pages under the cursor. */
|
|
542
|
+
async #eachLabelled(verb, filter, includeStopped, options, act) {
|
|
496
543
|
if (!filter?.labels || Object.keys(filter.labels).length === 0) {
|
|
497
544
|
const one = this.#product.noun.toLowerCase();
|
|
498
545
|
throw new RuntimeError({
|
|
499
|
-
message:
|
|
546
|
+
message: `${verb}All needs at least one label to match.`,
|
|
500
547
|
code: "invalid_request",
|
|
501
548
|
status: 0,
|
|
502
|
-
hint:
|
|
549
|
+
hint: `${verb === "stop" ? "Stop" : "Delete"} one ${one} with ${one}.${verb}(), or label the ones to ${verb} together.`,
|
|
503
550
|
});
|
|
504
551
|
}
|
|
505
|
-
const
|
|
506
|
-
// One key cannot name several
|
|
552
|
+
const matches = await (await this.list({ labels: filter.labels, limit: 100, ...(includeStopped ? { includeStopped } : {}) }, options)).toArray(Infinity);
|
|
553
|
+
// One key cannot name several acts: each makes its own.
|
|
507
554
|
const { idempotencyKey: _one, ...each } = options;
|
|
508
555
|
// In list order, oldest first, whichever finishes first.
|
|
509
|
-
const errors = new Array(
|
|
556
|
+
const errors = new Array(matches.length);
|
|
510
557
|
let next = 0;
|
|
511
558
|
const worker = async () => {
|
|
512
|
-
for (let at = next++; at <
|
|
513
|
-
await
|
|
559
|
+
for (let at = next++; at < matches.length; at = next++)
|
|
560
|
+
await act(matches[at], each).catch((error) => (errors[at] = error ?? "failed"));
|
|
514
561
|
};
|
|
515
|
-
await Promise.all(Array.from({ length: Math.min(8,
|
|
562
|
+
await Promise.all(Array.from({ length: Math.min(8, matches.length) }, worker));
|
|
516
563
|
return {
|
|
517
|
-
|
|
518
|
-
failed:
|
|
564
|
+
done: matches.filter((_, at) => errors[at] === undefined).map((m) => m.id),
|
|
565
|
+
failed: matches.flatMap((m, at) => errors[at] === undefined ? [] : [{ id: m.id, error: errors[at] }]),
|
|
519
566
|
};
|
|
520
567
|
}
|
|
521
568
|
}
|
package/dist/mcp.js
CHANGED
|
@@ -1,16 +1,61 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
|
-
import { resolveCredential } from "./credentials.js";
|
|
2
|
+
import { checkBrowserOrigins, connectionOrigins, resolveCredential, saveWalletConnection, } from "./credentials.js";
|
|
3
3
|
import { RuntimeError } from "./errors.js";
|
|
4
4
|
import { beginDeviceLogin } from "./login.js";
|
|
5
5
|
import { envFetch } from "./proxy.js";
|
|
6
6
|
import { apiOrigin, defaultBaseUrl, VERSION } from "./transport.js";
|
|
7
|
-
const CONNECT_INSTRUCTIONS = "Runtime is not connected on this machine yet. Call runtime_connect: it gives you a link and a code for the person you work for to approve in their browser (no key to copy). Once they have, call runtime_connect again; Runtime's tools then appear (sandboxes, commands, files and the rest).";
|
|
7
|
+
const CONNECT_INSTRUCTIONS = "Runtime is not connected on this machine yet. Call runtime_connect: it gives you a link and a code for the person you work for to approve in their browser (no key to copy). Once they have, call runtime_connect again; Runtime's tools then appear (sandboxes, commands, files and the rest). No person to approve? Open an account paid in USDC: runtime_wallet_open gives an address and the exact amount to send, and runtime_wallet_claim connects once the payment lands (in a shell, `npx withruntime billing topup 20 --new-account --accept-terms --wait` does the same; then call runtime_connect). There is no free trial on that path.";
|
|
8
8
|
const connectTool = {
|
|
9
9
|
name: "runtime_connect",
|
|
10
10
|
title: "Connect this machine to Runtime",
|
|
11
11
|
description: "Connect Runtime on this machine with a browser approval. The first call returns a link and a code: show them to the person you work for and ask them to approve. Call again after they have; it waits up to 45 seconds for the approval, then Runtime's tools appear.",
|
|
12
12
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
13
13
|
};
|
|
14
|
+
const walletOpenTool = {
|
|
15
|
+
name: "runtime_wallet_open",
|
|
16
|
+
title: "Open a Runtime account paid with a wallet",
|
|
17
|
+
description: "For an agent with no person to approve a browser login: opens a Runtime account and its first top-up, paid in USDC on Base, Solana or Tempo. No free trial: the account gets its key only once the payment lands. Answers with an address per network, the exact amount to send and a claim code, shown once: keep it secret, it is the only way to the account. Send exactly that amount from any wallet, then call runtime_wallet_claim with the claim code. Opening an account means agreeing to https://withruntime.com/legal/terms for whoever you act for.",
|
|
18
|
+
inputSchema: {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
usd: {
|
|
22
|
+
type: "number",
|
|
23
|
+
description: "Dollars of credit to buy: $10 to $500 in an account's first week.",
|
|
24
|
+
},
|
|
25
|
+
acceptTerms: {
|
|
26
|
+
type: "boolean",
|
|
27
|
+
description: "true agrees to https://withruntime.com/legal/terms. Required.",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
required: ["usd", "acceptTerms"],
|
|
31
|
+
additionalProperties: false,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
const walletClaimTool = {
|
|
35
|
+
name: "runtime_wallet_claim",
|
|
36
|
+
title: "Connect to a Runtime account opened with a wallet",
|
|
37
|
+
description: "Once a runtime_wallet_open top-up is paid, saves the account's key on this machine (not shown) and connects; Runtime's tools then appear. Until the payment lands it waits up to 45 seconds and says the top-up's status: call it again.",
|
|
38
|
+
inputSchema: {
|
|
39
|
+
type: "object",
|
|
40
|
+
properties: {
|
|
41
|
+
claimCode: { type: "string", description: "The claim code runtime_wallet_open gave." },
|
|
42
|
+
},
|
|
43
|
+
required: ["claimCode"],
|
|
44
|
+
additionalProperties: false,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
/** Where to send a wallet account's first top-up, for an agent to act on. */
|
|
48
|
+
function describeOpened(opened) {
|
|
49
|
+
const lines = [
|
|
50
|
+
`Opened a Runtime account. Send exactly ${opened.amount} USDC (for $${opened.amountUsd} of credit), on one of these networks:`,
|
|
51
|
+
"",
|
|
52
|
+
];
|
|
53
|
+
for (const network of opened.networks)
|
|
54
|
+
for (const token of network.tokens)
|
|
55
|
+
lines.push(`- ${network.network}: ${token.currency.toUpperCase()} to ${network.address} (token contract ${token.contract})`);
|
|
56
|
+
lines.push("", ...(opened.payBy ? [`Send before ${opened.payBy}.`] : []), "A different amount, token or network cannot be matched or returned automatically.", "", `Claim code (shown once, keep it secret): ${opened.claimCode}`, "Once sent, call runtime_wallet_claim with this claim code: it connects when the payment lands, a few minutes after the transfer.", "A person can take the account over at https://withruntime.com/account/claim with the same code.");
|
|
57
|
+
return lines.join("\n");
|
|
58
|
+
}
|
|
14
59
|
/** Bridges an MCP client that speaks stdio to Runtime's hosted MCP endpoint.
|
|
15
60
|
* Newline-delimited JSON-RPC in, the same out; every message is forwarded as
|
|
16
61
|
* it is, so the server's tools, schemas and `instructions` reach the client
|
|
@@ -115,7 +160,86 @@ export async function serveMcp(env = process.env, options = {}) {
|
|
|
115
160
|
for (const reply of answer.messages)
|
|
116
161
|
write(reply);
|
|
117
162
|
}
|
|
118
|
-
/**
|
|
163
|
+
/** The website's wallet sign-up, at the same origins a login would use. */
|
|
164
|
+
function wallet() {
|
|
165
|
+
const origins = connectionOrigins(env);
|
|
166
|
+
checkBrowserOrigins(origins);
|
|
167
|
+
const walletOptions = {
|
|
168
|
+
authUrl: origins.auth,
|
|
169
|
+
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
170
|
+
};
|
|
171
|
+
return {
|
|
172
|
+
open: async (input) => (await import("./wallet.js")).openWalletAccount(input, walletOptions),
|
|
173
|
+
claim: async (code) => (await import("./wallet.js")).claimWalletAccount(code, walletOptions),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/** A wallet account's claim: once paid, saves its key and connects. Waits up
|
|
177
|
+
* to 45 seconds for a payment on its way, as runtime_connect does. */
|
|
178
|
+
async function claimWallet(code) {
|
|
179
|
+
const until = Date.now() + 45_000;
|
|
180
|
+
for (;;) {
|
|
181
|
+
const claim = await wallet().claim(code);
|
|
182
|
+
if (claim.status === "paid") {
|
|
183
|
+
// The website issues the key once: a save that fails must not lose it.
|
|
184
|
+
let unsaved;
|
|
185
|
+
try {
|
|
186
|
+
await saveWalletConnection(env, claim.apiKey, claim.orgId);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
unsaved = error.message;
|
|
190
|
+
}
|
|
191
|
+
const said = await connected(claim.apiKey, "a wallet account");
|
|
192
|
+
return [
|
|
193
|
+
unsaved === undefined
|
|
194
|
+
? said
|
|
195
|
+
: `${said}\nThe key could not be saved (${unsaved}), and it is issued only once: keep it secret and safe now, as RUNTIME_API_KEY: ${claim.apiKey}`,
|
|
196
|
+
];
|
|
197
|
+
}
|
|
198
|
+
if (claim.status === "expired" || claim.status === "failed")
|
|
199
|
+
return [
|
|
200
|
+
`The top-up ${claim.status}: nothing was credited. Open a new one with runtime_wallet_open.`,
|
|
201
|
+
true,
|
|
202
|
+
];
|
|
203
|
+
// A top-up that never reached Stripe has no address: waiting cannot help.
|
|
204
|
+
if (claim.topup?.status === "open" && !claim.topup.networks.length)
|
|
205
|
+
return [claim.next ?? "This top-up has no address to pay to yet.", true];
|
|
206
|
+
if (Date.now() + 10_000 > until)
|
|
207
|
+
return [
|
|
208
|
+
claim.status === "pending"
|
|
209
|
+
? "The payment has arrived and is being confirmed. Call runtime_wallet_claim again in a minute."
|
|
210
|
+
: "Not paid yet. Once the transfer has gone through, call runtime_wallet_claim again with the same claim code.",
|
|
211
|
+
];
|
|
212
|
+
await new Promise((resolve) => setTimeout(resolve, 10_000));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/** Starts forwarding with this key and tells the client its tools changed.
|
|
216
|
+
* The client read CONNECT_INSTRUCTIONS on its own initialize and will not
|
|
217
|
+
* read instructions again, so the hosted server's (limits, safe writes,
|
|
218
|
+
* cleanup, the task) come back in this answer, or an agent's first session
|
|
219
|
+
* never sees them. Not every client re-lists tools when told; a restart of
|
|
220
|
+
* this server finds the saved key and serves them at once. */
|
|
221
|
+
async function connected(found, agentName) {
|
|
222
|
+
key = found;
|
|
223
|
+
pending = undefined;
|
|
224
|
+
const { messages } = await post(JSON.stringify({
|
|
225
|
+
jsonrpc: "2.0",
|
|
226
|
+
id: "runtime-connect",
|
|
227
|
+
method: "initialize",
|
|
228
|
+
params: initialize ?? {},
|
|
229
|
+
}));
|
|
230
|
+
await post(JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }));
|
|
231
|
+
write({ jsonrpc: "2.0", method: "notifications/tools/list_changed" });
|
|
232
|
+
const instructions = messages
|
|
233
|
+
.map((reply) => reply.result?.instructions)
|
|
234
|
+
.find((words) => typeof words === "string" && words.length > 0);
|
|
235
|
+
return [
|
|
236
|
+
`Connected${agentName ? ` as ${agentName}` : ""}. Runtime's tools are available now: list the tools again, then start with runtime_sandboxes_create. If they do not appear, restart this MCP server (or the client): the connection is saved, so it connects at once.`,
|
|
237
|
+
...(instructions
|
|
238
|
+
? ["", "Runtime's instructions, to follow from here:", "", instructions]
|
|
239
|
+
: []),
|
|
240
|
+
].join("\n");
|
|
241
|
+
}
|
|
242
|
+
/** Not connected yet: answer the client here, with the tools that connect. */
|
|
119
243
|
async function connectMode(message, id) {
|
|
120
244
|
const method = message.method;
|
|
121
245
|
const reply = (result) => write({ jsonrpc: "2.0", id, result });
|
|
@@ -133,38 +257,51 @@ export async function serveMcp(env = process.env, options = {}) {
|
|
|
133
257
|
if (method === "ping")
|
|
134
258
|
return reply({});
|
|
135
259
|
if (method === "tools/list")
|
|
136
|
-
return reply({ tools: [connectTool] });
|
|
260
|
+
return reply({ tools: [connectTool, walletOpenTool, walletClaimTool] });
|
|
261
|
+
const params = (message.params ?? {});
|
|
137
262
|
if (method !== "tools/call" ||
|
|
138
|
-
|
|
263
|
+
!["runtime_connect", "runtime_wallet_open", "runtime_wallet_claim"].includes(params.name))
|
|
139
264
|
return write(failure(id, "Runtime is not connected yet: call runtime_connect first."));
|
|
140
|
-
const text = (words) => reply({ content: [{ type: "text", text: words }] });
|
|
265
|
+
const text = (words, isError = false) => reply({ content: [{ type: "text", text: words }], ...(isError ? { isError } : {}) });
|
|
266
|
+
if (params.name !== "runtime_connect") {
|
|
267
|
+
const input = (params.arguments ?? {});
|
|
268
|
+
try {
|
|
269
|
+
if (params.name === "runtime_wallet_open") {
|
|
270
|
+
if (input.acceptTerms !== true)
|
|
271
|
+
return text("Opening an account means agreeing to https://withruntime.com/legal/terms. Call again with acceptTerms: true to agree to them for whoever you act for.", true);
|
|
272
|
+
if (typeof input.usd !== "number" || !(input.usd > 0))
|
|
273
|
+
return text("Say how many dollars of credit to buy in usd, such as 20.", true);
|
|
274
|
+
return text(describeOpened(await wallet().open({ usd: input.usd, acceptTerms: true })));
|
|
275
|
+
}
|
|
276
|
+
if (typeof input.claimCode !== "string" || !input.claimCode.trim())
|
|
277
|
+
return text("Give the claimCode runtime_wallet_open answered.", true);
|
|
278
|
+
return text(...(await claimWallet(input.claimCode.trim())));
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
return text(error instanceof Error ? error.message : String(error), true);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
141
284
|
try {
|
|
285
|
+
// A key saved since this server started (a wallet account's claim, a
|
|
286
|
+
// login in a shell) connects without a browser.
|
|
287
|
+
const saved = await resolveCredential(env).catch(() => undefined);
|
|
288
|
+
if (saved)
|
|
289
|
+
return text(await connected(saved));
|
|
142
290
|
if (!pending) {
|
|
143
291
|
pending = await beginDeviceLogin(env, env.RUNTIME_AGENT_NAME ?? "MCP agent", {
|
|
144
292
|
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
145
293
|
});
|
|
146
294
|
if (!env.RUNTIME_NO_BROWSER && !pending.resumed)
|
|
147
295
|
await (options.openBrowser ?? openBrowser)(pending.url).catch(() => undefined);
|
|
148
|
-
return text(`Ask the person you work for to open ${pending.url} (it may already be open in their browser), sign in to Runtime, check that it shows the code ${pending.userCode}, and choose Connect agent. Then call runtime_connect again.`);
|
|
296
|
+
return text(`Ask the person you work for to open ${pending.url} (it may already be open in their browser), sign in to Runtime, check that it shows the code ${pending.userCode}, and choose Connect agent. Then call runtime_connect again. No person to approve? See this server's instructions to open an account paid in USDC.`);
|
|
149
297
|
}
|
|
150
298
|
const until = Math.min(pending.expiresAt, Date.now() + 45_000);
|
|
151
299
|
while (Date.now() < until) {
|
|
152
300
|
const outcome = await pending.check();
|
|
153
301
|
if (outcome === "expired")
|
|
154
302
|
break;
|
|
155
|
-
if (outcome !== "pending")
|
|
156
|
-
|
|
157
|
-
pending = undefined;
|
|
158
|
-
await post(JSON.stringify({
|
|
159
|
-
jsonrpc: "2.0",
|
|
160
|
-
id: "runtime-connect",
|
|
161
|
-
method: "initialize",
|
|
162
|
-
params: initialize ?? {},
|
|
163
|
-
}));
|
|
164
|
-
await post(JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }));
|
|
165
|
-
write({ jsonrpc: "2.0", method: "notifications/tools/list_changed" });
|
|
166
|
-
return text(`Connected as ${outcome.agentName}. Runtime's tools are available now: list the tools again, then start with runtime_sandboxes_create.`);
|
|
167
|
-
}
|
|
303
|
+
if (outcome !== "pending")
|
|
304
|
+
return text(await connected(await resolveCredential(env), outcome.agentName));
|
|
168
305
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
169
306
|
}
|
|
170
307
|
if (Date.now() >= pending.expiresAt) {
|
package/dist/modal/index.d.ts
CHANGED
|
@@ -47,6 +47,7 @@ export declare class App {
|
|
|
47
47
|
readonly name?: string | undefined;
|
|
48
48
|
readonly environmentName?: string | undefined;
|
|
49
49
|
constructor(appId: string, name?: string | undefined, environmentName?: string | undefined);
|
|
50
|
+
readonly info: RefusedAsync;
|
|
50
51
|
}
|
|
51
52
|
export declare class AppService {
|
|
52
53
|
private readonly client;
|
|
@@ -90,6 +91,7 @@ export declare class VolumeService {
|
|
|
90
91
|
fromName(name: string, params?: {
|
|
91
92
|
createIfMissing?: boolean;
|
|
92
93
|
environment?: string;
|
|
94
|
+
version?: 1 | 2;
|
|
93
95
|
}): Promise<Volume>;
|
|
94
96
|
delete(name: string, params?: {
|
|
95
97
|
allowMissing?: boolean;
|
|
@@ -211,6 +213,14 @@ export declare class SandboxSnapshotService {
|
|
|
211
213
|
constructor(client: ModalClient);
|
|
212
214
|
fromId(snapshotId: string): Promise<SandboxSnapshot>;
|
|
213
215
|
}
|
|
216
|
+
/** `client.environments` (modal 0.11.1): refused, as `environment` options are. */
|
|
217
|
+
export declare class EnvironmentService {
|
|
218
|
+
readonly fromName: RefusedAsync;
|
|
219
|
+
readonly objects: Refused;
|
|
220
|
+
}
|
|
221
|
+
export declare const Environment: Refused;
|
|
222
|
+
export declare const EnvironmentAppsService: Refused;
|
|
223
|
+
export declare const EnvironmentObjectsService: Refused;
|
|
214
224
|
export type ModalClientParams = ClientOptions & {
|
|
215
225
|
tokenId?: string;
|
|
216
226
|
tokenSecret?: string;
|
|
@@ -237,6 +247,7 @@ export declare class ModalClient {
|
|
|
237
247
|
readonly cloudBucketMounts: CloudBucketMountService;
|
|
238
248
|
readonly sandboxSnapshots: SandboxSnapshotService;
|
|
239
249
|
readonly proxies: ProxyService;
|
|
250
|
+
readonly environments: EnvironmentService;
|
|
240
251
|
constructor(options?: ModalClientParams);
|
|
241
252
|
/** Throws once close() was called, as Modal's client does. */
|
|
242
253
|
open(): void;
|
|
@@ -292,11 +303,15 @@ export declare class SandboxService {
|
|
|
292
303
|
create(app: App, image: Image, params?: SandboxCreateParams): Promise<Sandbox>;
|
|
293
304
|
experimentalCreate(app: App, image: Image, params?: SandboxCreateParams): Promise<Sandbox>;
|
|
294
305
|
fromId(id: string): Promise<Sandbox>;
|
|
306
|
+
/** The running sandbox of that name; with includeTerminated, when none
|
|
307
|
+
* runs, the one that stopped last (modal 0.11.1). */
|
|
295
308
|
fromName(appName: string, name: string, params?: {
|
|
296
309
|
environment?: string;
|
|
310
|
+
includeTerminated?: boolean;
|
|
297
311
|
}): Promise<Sandbox>;
|
|
298
312
|
experimentalFromName(appName: string, name: string, params?: {
|
|
299
313
|
environment?: string;
|
|
314
|
+
includeTerminated?: boolean;
|
|
300
315
|
}): Promise<Sandbox>;
|
|
301
316
|
/** A new sandbox continuing a memory snapshot: its files, memory and
|
|
302
317
|
* running entrypoint, under the snapshot's app and tags. */
|