withruntime 0.14.0 → 0.16.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.
Files changed (62) hide show
  1. package/BLAXEL.md +97 -32
  2. package/COMPATIBILITY.md +120 -5
  3. package/DAYTONA.md +34 -3
  4. package/E2B.md +34 -5
  5. package/README.md +33 -0
  6. package/README.src.md +33 -0
  7. package/VERCEL.md +52 -5
  8. package/compatibility-lock.json +71 -111
  9. package/dist/blaxel/client.d.ts +11 -0
  10. package/dist/blaxel/client.js +28 -0
  11. package/dist/blaxel/errors.d.ts +65 -0
  12. package/dist/blaxel/errors.js +114 -0
  13. package/dist/blaxel/index.d.ts +10 -3
  14. package/dist/blaxel/index.js +23 -4
  15. package/dist/blaxel/preview.d.ts +29 -4
  16. package/dist/blaxel/preview.js +79 -5
  17. package/dist/blaxel/sandbox.js +1 -0
  18. package/dist/blaxel/session.js +4 -2
  19. package/dist/cli-extras.js +2 -1
  20. package/dist/cli-words.js +1 -0
  21. package/dist/cli.d.ts +1 -1
  22. package/dist/cli.js +61 -26
  23. package/dist/client.d.ts +1 -1
  24. package/dist/daytona/client.d.ts +4 -0
  25. package/dist/daytona/client.js +8 -3
  26. package/dist/daytona/daytona.d.ts +3 -0
  27. package/dist/daytona/daytona.js +2 -1
  28. package/dist/e2b/sandbox.d.ts +6 -5
  29. package/dist/e2b/sandbox.js +9 -12
  30. package/dist/freestyle/index.d.ts +77 -5
  31. package/dist/freestyle/index.js +380 -48
  32. package/dist/identity.js +63 -22
  33. package/dist/index.d.ts +2 -2
  34. package/dist/machine.d.ts +28 -13
  35. package/dist/machine.js +49 -15
  36. package/dist/mcp.js +16 -3
  37. package/dist/modal/index.d.ts +15 -0
  38. package/dist/modal/index.js +35 -7
  39. package/dist/morph/index.d.ts +255 -0
  40. package/dist/morph/index.js +641 -0
  41. package/dist/network-cli.js +2 -2
  42. package/dist/product-names.d.ts +2 -2
  43. package/dist/product-names.js +2 -2
  44. package/dist/products/limits.d.ts +16 -2
  45. package/dist/products/limits.js +2 -2
  46. package/dist/products/mcp.d.ts +5 -0
  47. package/dist/products/network-products.d.ts +21 -3
  48. package/dist/products/network-products.js +3 -1
  49. package/dist/products/previews.d.ts +6 -3
  50. package/dist/products/previews.js +1 -1
  51. package/dist/snapshots.d.ts +4 -0
  52. package/dist/ssh.d.ts +35 -0
  53. package/dist/ssh.js +93 -1
  54. package/dist/transport.d.ts +11 -8
  55. package/dist/transport.js +46 -12
  56. package/dist/types.d.ts +24 -0
  57. package/dist/vercel/command.d.ts +48 -1
  58. package/dist/vercel/command.js +237 -6
  59. package/dist/vercel/sandbox-user.js +2 -1
  60. package/dist/vercel/sandbox.d.ts +16 -2
  61. package/dist/vercel/sandbox.js +45 -2
  62. package/package.json +6 -1
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
- wake(options?: RequestOptions & {
62
- wait?: boolean;
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 an hour ahead of now). A
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 or persistence;
70
- * fields left out stay as they are. `persistent: true` keeps it running
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),
@@ -92,9 +95,12 @@ export declare abstract class Machine<Info extends MachineInfo, Settings extends
92
95
  * while a snapshot or fork of it is being taken. `restart` is optional:
93
96
  * true is the only value, and it is sent either way, so an API from before
94
97
  * it was optional accepts the call too. */
95
- resize(size: {
98
+ resize(
99
+ /** diskMiB only grows; the filesystem fills it at the restart. */
100
+ size: {
96
101
  vcpu?: number;
97
102
  memoryMiB?: number;
103
+ diskMiB?: number;
98
104
  }, options?: RequestOptions & {
99
105
  restart?: true;
100
106
  }): Promise<this>;
@@ -116,10 +122,10 @@ export declare abstract class Machine<Info extends MachineInfo, Settings extends
116
122
  * `timeoutMs` (ten minutes unless given) mid-capture, it stays paused and
117
123
  * the error names the snapshot. */
118
124
  snapshot(options?: SnapshotOptions & RequestOptions): Promise<Snapshot>;
119
- /** Starts a stopped persistent sandbox again from its disk (memory is not kept). */
120
- restart(options?: RequestOptions & {
121
- wait?: boolean;
122
- }): Promise<this>;
125
+ /** Starts a stopped persistent sandbox again from its disk (memory is not
126
+ * kept), waiting for room on a full server as `wake` does. Its image's
127
+ * start command runs again, reported in `info.start`. */
128
+ restart(options?: LifecycleOptions): Promise<this>;
123
129
  /** The end of an `await using` block. A sandbox this client created is
124
130
  * deleted, as E2B's block kills it, unless it was created with
125
131
  * `keep: true`; one it found with `get` or `getOrCreate` is stopped, which
@@ -137,6 +143,15 @@ export type CreateOptions = RequestOptions & {
137
143
  * next try: to tell a person why nothing has happened yet. */
138
144
  onCapacityWait?: (refusal: RuntimeError, waitMs: number) => void;
139
145
  };
146
+ /** How `wake` and `restart` wait when the sandbox's server is full for now. */
147
+ export type LifecycleOptions = RequestOptions & {
148
+ wait?: boolean;
149
+ /** How long to keep retrying `no_capacity` and the like. Default: the
150
+ * client's, two minutes; 0 fails at once. */
151
+ waitForCapacityMs?: number;
152
+ /** Called before each wait, with the refusal and the milliseconds until the next try. */
153
+ onCapacityWait?: (refusal: RuntimeError, waitMs: number) => void;
154
+ };
140
155
  /** A machine product's collection: `runtime.<product>`. */
141
156
  export declare abstract class Machines<M extends Machine<Info, object, Deleted>, Info extends MachineInfo, Create extends {
142
157
  name?: string;
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
- * (Marc, 6 October 2026: deleted unless the code passes `keep: true`). One
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 an hour ahead of now). A
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 or persistence;
138
- * fields left out stay as they are. `persistent: true` keeps it running
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
  }
@@ -171,7 +175,9 @@ export class Machine {
171
175
  * while a snapshot or fork of it is being taken. `restart` is optional:
172
176
  * true is the only value, and it is sent either way, so an API from before
173
177
  * it was optional accepts the call too. */
174
- async resize(size, options = {}) {
178
+ async resize(
179
+ /** diskMiB only grows; the filesystem fills it at the restart. */
180
+ size, options = {}) {
175
181
  const { restart = true, ...rest } = options;
176
182
  this.#keep(await this.#t.json({
177
183
  method: "POST",
@@ -277,20 +283,26 @@ export class Machine {
277
283
  await this.refresh(request());
278
284
  // Straight after a fork or a wake the sandbox is still `resuming`, and
279
285
  // after a pause still `pausing`: wait for where it is going, or a
280
- // snapshot of it is refused as not paused (user lane, 23 September 2026).
286
+ // snapshot of it is refused as not paused (23 September 2026).
281
287
  if (this.state === "resuming" || this.state === "starting")
282
288
  await this.waitFor("running", request());
283
289
  else if (this.state === "pausing")
284
290
  await this.waitFor("paused", request());
285
291
  running = this.state === "running";
286
292
  // Check before entering cleanup: an abort during refresh must not pause or wake.
287
- const pauseOptions = running ? request() : undefined;
293
+ request();
288
294
  let captured;
289
295
  let primary;
290
296
  let failed = false;
297
+ /** Whether the server may have left a running source paused: it answered
298
+ * before the capture was over, or not at all. */
299
+ let leftPaused = running;
291
300
  try {
292
- if (running)
293
- await this.pause(pauseOptions);
301
+ // One call: the server pauses a running source for the capture, and
302
+ // wakes it before it answers a capture that is over (ready or failed).
303
+ // Pausing, snapshotting and waking it from here took four calls: a
304
+ // repeat snapshot of a 4 GiB sandbox took 1.96 s at the median against
305
+ // 1.19 s for this one (15 October 2026).
294
306
  let snapshot = await this.#t.json({
295
307
  method: "POST",
296
308
  path: this.#path(":snapshot"),
@@ -300,6 +312,7 @@ export class Machine {
300
312
  ...request(),
301
313
  });
302
314
  captured = snapshot;
315
+ leftPaused = running && snapshot.state === "capturing";
303
316
  // Prefer: wait is bounded on the server. A capture still in progress
304
317
  // must keep its source paused, or the worker refuses it after we wake.
305
318
  while (snapshot.state === "capturing") {
@@ -337,9 +350,17 @@ export class Machine {
337
350
  }
338
351
  // A capture still running needs its source paused: the worker fails one
339
352
  // whose source woke. The timeout says so and leaves the wake to the caller.
340
- if (running && !late) {
353
+ if (leftPaused && !late) {
341
354
  try {
342
- await this.wake();
355
+ // A call that failed may have been refused before anything paused
356
+ // (pausable: false, volumes): only a source that is paused is woken.
357
+ if (failed && captured === undefined)
358
+ await this.refresh();
359
+ if (!failed ||
360
+ captured !== undefined ||
361
+ this.state === "paused" ||
362
+ this.state === "pausing")
363
+ await this.wake();
343
364
  }
344
365
  catch (wakeError) {
345
366
  const recovery = {
@@ -365,17 +386,30 @@ export class Machine {
365
386
  throw primary;
366
387
  return captured;
367
388
  }
368
- /** Starts a stopped persistent sandbox again from its disk (memory is not kept). */
389
+ /** Starts a stopped persistent sandbox again from its disk (memory is not
390
+ * kept), waiting for room on a full server as `wake` does. Its image's
391
+ * start command runs again, reported in `info.start`. */
369
392
  async restart(options = {}) {
370
393
  return this.#lifecycle("restart", options);
371
394
  }
372
395
  async #lifecycle(verb, options, body = {}) {
396
+ // A wake or restart refused for room ran nothing and is sent again with
397
+ // the same key (api.md, "A refusal is not kept"). Until 17 October 2026
398
+ // only create waited: the product check's wake was refused no_capacity
399
+ // in 36 ms on 16 October, where a create would have waited for room.
400
+ const room = verb === "wake" || verb === "restart"
401
+ ? {
402
+ waitForCapacityMs: options.waitForCapacityMs ?? this.#t.waitForCapacityMs,
403
+ ...(options.onCapacityWait ? { onCapacityWait: options.onCapacityWait } : {}),
404
+ }
405
+ : {};
373
406
  this.#keep(await this.#t.json({
374
407
  method: "POST",
375
408
  path: this.#path(`:${verb}`),
376
409
  body,
377
410
  wait: options.wait === false ? 0 : 60,
378
411
  ...requestOnly(options),
412
+ ...room,
379
413
  }));
380
414
  return this;
381
415
  }
package/dist/mcp.js CHANGED
@@ -212,11 +212,16 @@ export async function serveMcp(env = process.env, options = {}) {
212
212
  await new Promise((resolve) => setTimeout(resolve, 10_000));
213
213
  }
214
214
  }
215
- /** Starts forwarding with this key and tells the client its tools changed. */
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. */
216
221
  async function connected(found, agentName) {
217
222
  key = found;
218
223
  pending = undefined;
219
- await post(JSON.stringify({
224
+ const { messages } = await post(JSON.stringify({
220
225
  jsonrpc: "2.0",
221
226
  id: "runtime-connect",
222
227
  method: "initialize",
@@ -224,7 +229,15 @@ export async function serveMcp(env = process.env, options = {}) {
224
229
  }));
225
230
  await post(JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }));
226
231
  write({ jsonrpc: "2.0", method: "notifications/tools/list_changed" });
227
- return `Connected${agentName ? ` as ${agentName}` : ""}. Runtime's tools are available now: list the tools again, then start with runtime_sandboxes_create.`;
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");
228
241
  }
229
242
  /** Not connected yet: answer the client here, with the tools that connect. */
230
243
  async function connectMode(message, id) {
@@ -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. */
@@ -1,5 +1,5 @@
1
1
  import { ConflictError as NativeConflictError, InvalidRequestError, NotFoundError as NativeNotFoundError, RateLimitError, RuntimeError, ServiceUnavailableError, } from "../errors.js";
2
- import { all, client, CompatibilityError, create, destroy, execOptions, only, refuse, region, } from "../compat/core.js";
2
+ import { all, client, CompatibilityError, create, destroy, execOptions, only, refuse, refusal, refused, region, } from "../compat/core.js";
3
3
  import { GuestFiles } from "../compat/files.js";
4
4
  export { CompatibilityError };
5
5
  // Errors ----------------------------------------------------------------------
@@ -104,6 +104,9 @@ export class App {
104
104
  this.environmentName = environmentName;
105
105
  }
106
106
  }
107
+ refuse("Modal", "App", App, {
108
+ info: "a Runtime app is a label on its sandboxes, with no lifecycle, Functions or Servers: list them with client.sandboxes.list({ appId: app.appId })",
109
+ });
107
110
  export class AppService {
108
111
  client;
109
112
  constructor(client) {
@@ -183,9 +186,11 @@ export class VolumeService {
183
186
  }
184
187
  async fromName(name, params = {}) {
185
188
  this.client.open();
186
- only("Modal volume", params, ["createIfMissing", "environment"]);
189
+ only("Modal volume", params, ["createIfMissing", "environment", "version"]);
187
190
  if (params.environment)
188
191
  throw new CompatibilityError("Modal", "named Modal environments");
192
+ if (params.version !== undefined)
193
+ throw new CompatibilityError("Modal", "VolumeFS versions; a Runtime volume has one format, so omit version");
189
194
  const found = await this.find(name);
190
195
  if (found)
191
196
  return new Volume(found.id, name);
@@ -402,6 +407,21 @@ export class SandboxSnapshotService {
402
407
  return new SandboxSnapshot(snapshot.id);
403
408
  }
404
409
  }
410
+ // Environments ------------------------------------------------------------------
411
+ const NO_ENVIRONMENTS = "Runtime has no Modal environments: an account's sandboxes share one namespace, grouped by app name (client.apps.fromName) and tags";
412
+ /** `client.environments` (modal 0.11.1): refused, as `environment` options are. */
413
+ export class EnvironmentService {
414
+ }
415
+ refuse("Modal", "EnvironmentService", EnvironmentService, { fromName: NO_ENVIRONMENTS });
416
+ Object.defineProperty(EnvironmentService.prototype, "objects", {
417
+ configurable: true,
418
+ get() {
419
+ throw refusal("Modal", "EnvironmentService.objects", NO_ENVIRONMENTS);
420
+ },
421
+ });
422
+ export const Environment = refused("Modal", "Environment", NO_ENVIRONMENTS);
423
+ export const EnvironmentAppsService = refused("Modal", "EnvironmentAppsService", NO_ENVIRONMENTS);
424
+ export const EnvironmentObjectsService = refused("Modal", "EnvironmentObjectsService", NO_ENVIRONMENTS);
405
425
  export class ModalClient {
406
426
  runtime;
407
427
  apps;
@@ -412,6 +432,7 @@ export class ModalClient {
412
432
  cloudBucketMounts = new CloudBucketMountService();
413
433
  sandboxSnapshots;
414
434
  proxies = new ProxyService();
435
+ environments = new EnvironmentService();
415
436
  #closed = false;
416
437
  constructor(options = {}) {
417
438
  if (options.endpoint !== undefined ||
@@ -458,7 +479,7 @@ export class ModalClient {
458
479
  }
459
480
  /** The Modal SDK version whose interface this adapter implements. */
460
481
  version() {
461
- return "0.11.0";
482
+ return "0.11.1";
462
483
  }
463
484
  close() {
464
485
  // The transport owns no background connection; later calls fail as Modal's do.
@@ -619,14 +640,21 @@ export class SandboxService {
619
640
  throw new NotFoundError("Sandbox is not a Modal-compatible sandbox");
620
641
  return new Sandbox(s, new ContainerProcess(await modal(() => s.processes.get(p)), "text"), this.client);
621
642
  }
643
+ /** The running sandbox of that name; with includeTerminated, when none
644
+ * runs, the one that stopped last (modal 0.11.1). */
622
645
  async fromName(appName, name, params = {}) {
623
646
  this.client?.open();
624
- only("Modal sandbox name", params, ["environment"]);
647
+ only("Modal sandbox name", params, ["environment", "includeTerminated"]);
625
648
  if (params.environment)
626
649
  throw new CompatibilityError("Modal", ENVIRONMENTS);
627
- const s = (await all(this.runtime, "modal")).find((s) => s.info.labels["modal.app"] === appName &&
628
- s.info.labels["modal.name"] === name &&
629
- s.state === "running");
650
+ const named = (await all(this.runtime, "modal")).filter((s) => s.info.labels["modal.app"] === appName && s.info.labels["modal.name"] === name);
651
+ const ended = (s) => s.info.endedAt ?? s.info.createdAt ?? "";
652
+ const s = named.find((s) => s.state === "running") ??
653
+ (params.includeTerminated
654
+ ? named
655
+ .filter((s) => s.state === "stopped")
656
+ .sort((a, b) => ended(b).localeCompare(ended(a)))[0]
657
+ : undefined);
630
658
  if (!s)
631
659
  throw new NotFoundError(`Sandbox ${name} not found`);
632
660
  return this.fromId(s.id);
@@ -0,0 +1,255 @@
1
+ import type { Runtime } from "../client.js";
2
+ import type { Preview } from "../products/previews.js";
3
+ import { Sandbox } from "../sandbox.js";
4
+ import type { Snapshot as NativeSnapshot } from "../snapshots.js";
5
+ import { CompatibilityError, type ClientOptions, type RefusedAsync } from "../compat/core.js";
6
+ export { CompatibilityError };
7
+ /** Morph's base image; on Runtime, the default image. */
8
+ export declare const MORPH_DEFAULT_IMAGE = "morphvm-minimal";
9
+ export declare enum SnapshotStatus {
10
+ PENDING = "pending",
11
+ READY = "ready",
12
+ FAILED = "failed",
13
+ DELETING = "deleting",
14
+ DELETED = "deleted"
15
+ }
16
+ export declare enum InstanceStatus {
17
+ PENDING = "pending",
18
+ READY = "ready",
19
+ PAUSED = "paused",
20
+ SAVING = "saving",
21
+ ERROR = "error"
22
+ }
23
+ export interface ResourceSpec {
24
+ vcpus: number;
25
+ /** MiB. */
26
+ memory: number;
27
+ /** MiB. */
28
+ diskSize: number;
29
+ }
30
+ export interface SnapshotRefs {
31
+ imageId: string;
32
+ }
33
+ export interface InstanceHttpService {
34
+ name: string;
35
+ port: number;
36
+ url: string;
37
+ }
38
+ export interface InstanceNetworking {
39
+ internalIp?: string;
40
+ httpServices: InstanceHttpService[];
41
+ auth_mode?: string;
42
+ }
43
+ export interface InstanceRefs {
44
+ snapshotId: string;
45
+ imageId: string;
46
+ }
47
+ export interface InstanceExecResponse {
48
+ exit_code: number;
49
+ stdout: string;
50
+ stderr: string;
51
+ }
52
+ export interface InstanceTTL {
53
+ ttlSeconds?: number;
54
+ ttlExpireAt?: number;
55
+ ttlAction?: "stop" | "pause";
56
+ }
57
+ export interface InstanceWakeOn {
58
+ wakeOnSsh?: boolean;
59
+ wakeOnHttp?: boolean;
60
+ }
61
+ export interface ExecOptions {
62
+ /** Seconds; default 24 hours, as Morph's. */
63
+ timeout?: number;
64
+ onStdout?: (content: string) => void;
65
+ onStderr?: (content: string) => void;
66
+ }
67
+ export interface InstanceSshKey {
68
+ object: "instance_ssh_key";
69
+ private_key: string;
70
+ public_key: string;
71
+ password: string;
72
+ }
73
+ export interface MorphCloudClientOptions extends ClientOptions {
74
+ /** A Runtime key (`rtcloud_...`); a Morph key is never sent anywhere. */
75
+ apiKey?: string;
76
+ baseUrl?: string;
77
+ verbose?: boolean;
78
+ }
79
+ export type ImageListOptions = Record<string, never>;
80
+ export interface SnapshotListOptions {
81
+ digest?: string;
82
+ metadata?: Record<string, string>;
83
+ }
84
+ export interface SnapshotCreateOptions {
85
+ imageId?: string;
86
+ vcpus?: number;
87
+ memory?: number;
88
+ diskSize?: number;
89
+ digest?: string;
90
+ metadata?: Record<string, string>;
91
+ }
92
+ export interface SnapshotGetOptions {
93
+ snapshotId: string;
94
+ }
95
+ export interface InstanceListOptions {
96
+ metadata?: Record<string, string>;
97
+ }
98
+ export interface InstanceStartOptions {
99
+ snapshotId: string;
100
+ metadata?: Record<string, string>;
101
+ ttlSeconds?: number;
102
+ ttlAction?: "stop" | "pause";
103
+ /** Seconds to wait until it runs. */
104
+ timeout?: number;
105
+ }
106
+ export interface InstanceSnapshotOptions {
107
+ digest?: string;
108
+ metadata?: Record<string, string>;
109
+ }
110
+ export interface InstanceGetOptions {
111
+ instanceId: string;
112
+ }
113
+ export interface InstanceStopOptions {
114
+ instanceId: string;
115
+ }
116
+ export interface InstanceBootOptions {
117
+ snapshotId: string;
118
+ vcpus?: number;
119
+ memory?: number;
120
+ diskSize?: number;
121
+ metadata?: Record<string, string>;
122
+ ttlSeconds?: number;
123
+ ttlAction?: "stop" | "pause";
124
+ }
125
+ export interface SyncOptions {
126
+ delete?: boolean;
127
+ dryRun?: boolean;
128
+ verbose?: boolean;
129
+ respectGitignore?: boolean;
130
+ }
131
+ export declare class Image {
132
+ readonly id: string;
133
+ readonly name: string;
134
+ readonly diskSize: number;
135
+ readonly created: number;
136
+ readonly description?: string | undefined;
137
+ readonly object: "image";
138
+ constructor(id: string, name: string, diskSize: number, created: number, description?: string | undefined);
139
+ }
140
+ export declare class Snapshot {
141
+ #private;
142
+ readonly native: NativeSnapshot;
143
+ readonly id: string;
144
+ readonly object: "snapshot";
145
+ readonly created: number;
146
+ readonly status: SnapshotStatus;
147
+ readonly spec: ResourceSpec;
148
+ readonly refs: SnapshotRefs;
149
+ readonly digest?: string;
150
+ metadata?: Record<string, string>;
151
+ constructor(native: NativeSnapshot, morph: MorphCloudClient);
152
+ delete(): Promise<void>;
153
+ /** Morph's chain hash: SHA-256 of the parent's hash, a newline and the effect. */
154
+ static computeChainHash(parentChainHash: string, effectIdentifier: string): string;
155
+ /** Runs `command` on a copy of this snapshot and snapshots the result, once:
156
+ * a later setup of the same command on the same snapshot answers the
157
+ * snapshot made the first time. A command that fails throws, and the copy
158
+ * is stopped either way. */
159
+ setup(command: string): Promise<Snapshot>;
160
+ /** Adds or changes these metadata keys; the others stay. */
161
+ setMetadata(metadata: Record<string, string>): Promise<void>;
162
+ }
163
+ export declare class Instance {
164
+ #private;
165
+ readonly id: string;
166
+ readonly object: "instance";
167
+ readonly created: number;
168
+ status: InstanceStatus;
169
+ readonly spec: ResourceSpec;
170
+ readonly refs: InstanceRefs;
171
+ networking: InstanceNetworking;
172
+ ttl: InstanceTTL;
173
+ wakeOn: InstanceWakeOn;
174
+ metadata?: Record<string, string>;
175
+ readonly ssh: RefusedAsync;
176
+ readonly sshKey: RefusedAsync;
177
+ readonly sshKeyRotate: RefusedAsync;
178
+ readonly reboot: RefusedAsync;
179
+ readonly setTTL: RefusedAsync;
180
+ constructor(sandbox: Sandbox, morph: MorphCloudClient, previews?: Preview[]);
181
+ /** The Runtime sandbox behind it, for what Morph has no call for. */
182
+ get native(): Sandbox;
183
+ /** Deletes it, with its disk and memory, as Morph's stop does; its snapshots stay. */
184
+ stop(): Promise<void>;
185
+ /** Keeps memory and disk, and stops compute charges. */
186
+ pause(): Promise<void>;
187
+ resume(): Promise<void>;
188
+ /** Adds or changes these metadata keys; the others stay. */
189
+ setMetadata(metadata: Record<string, string>): Promise<void>;
190
+ /** Runtime wakes a paused sandbox on any request (an API call, SSH, a
191
+ * visit to a shared port) or on none: both values must agree. */
192
+ setWakeOn(wakeOnSsh?: boolean, wakeOnHttp?: boolean): Promise<void>;
193
+ /** Files, memory and running processes; a running instance pauses for the
194
+ * moment it takes, then runs on. */
195
+ snapshot(options?: InstanceSnapshotOptions): Promise<Snapshot>;
196
+ /** `count` running copies, with its files, memory and processes, and the
197
+ * snapshot they were started from, which is kept. */
198
+ branch(count: number): Promise<{
199
+ snapshot: Snapshot;
200
+ instances: Instance[];
201
+ }>;
202
+ /** Shares `port` at an HTTPS address. With no auth_mode, anyone with the
203
+ * address reaches it (Runtime's public preview, which needs a kept
204
+ * top-up); with auth_mode "api_key", a request needs the preview's token
205
+ * in `x-runtime-preview-token` (native.previews.get(port)). */
206
+ exposeHttpService(name: string, port: number, auth_mode?: string): Promise<InstanceHttpService>;
207
+ hideHttpService(name: string): Promise<void>;
208
+ /** A string, or a one-item array, runs in a shell; a longer array runs as
209
+ * its arguments. Output streams to onStdout and onStderr as it comes. */
210
+ exec(command: string | string[], options?: ExecOptions): Promise<InstanceExecResponse>;
211
+ /** Seconds; waits until it runs, waking it if paused. */
212
+ waitUntilReady(timeout?: number): Promise<void>;
213
+ /** Copies a local file or folder into the instance, or out of it: the
214
+ * instance's side is written `<instance id>:<path>`. Without delete, dry
215
+ * runs or .gitignore, which refuse. */
216
+ sync(source: string, dest: string, options?: SyncOptions): Promise<void>;
217
+ }
218
+ /** Morph's client, on Runtime. `new MorphCloudClient()` reads RUNTIME_API_KEY
219
+ * (or a saved login), never MORPH_API_KEY. */
220
+ export declare class MorphCloudClient {
221
+ readonly runtime: Runtime;
222
+ readonly baseUrl: string;
223
+ readonly apiKey: string;
224
+ readonly verbose: boolean;
225
+ readonly GET: RefusedAsync;
226
+ readonly POST: RefusedAsync;
227
+ readonly streamPOST: RefusedAsync;
228
+ readonly DELETE: RefusedAsync;
229
+ constructor(options?: MorphCloudClientOptions);
230
+ images: {
231
+ /** Morph's base image, which is Runtime's default image. Build others with runtime.images. */
232
+ list: (_options?: ImageListOptions) => Promise<Image[]>;
233
+ };
234
+ snapshots: {
235
+ list: (options?: SnapshotListOptions) => Promise<Snapshot[]>;
236
+ /** A bootable snapshot of an image at a size: Runtime boots a sandbox from
237
+ * the image, snapshots its disk and stops it. With a digest, a snapshot
238
+ * already made with it is answered instead. */
239
+ create: (options?: SnapshotCreateOptions) => Promise<Snapshot>;
240
+ get: (options: SnapshotGetOptions) => Promise<Snapshot>;
241
+ };
242
+ instances: {
243
+ /** Running and paused instances whose metadata match. */
244
+ list: (options?: InstanceListOptions) => Promise<Instance[]>;
245
+ /** A running copy of a snapshot: files, and memory and processes when the
246
+ * snapshot kept them. */
247
+ start: (options: InstanceStartOptions) => Promise<Instance>;
248
+ /** As start; a different vcpus or memory restarts it at that size (its
249
+ * files kept, its programs started afresh). The disk keeps the snapshot's size. */
250
+ boot: (options: InstanceBootOptions) => Promise<Instance>;
251
+ get: (options: InstanceGetOptions) => Promise<Instance>;
252
+ /** Deletes it, with its disk and memory, as Morph's stop does; its snapshots stay. */
253
+ stop: (options: InstanceStopOptions) => Promise<void>;
254
+ };
255
+ }