withruntime 0.23.0 → 0.33.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.
Files changed (76) hide show
  1. package/COMPATIBILITY.md +64 -3
  2. package/E2B.md +1 -1
  3. package/README.md +36 -2
  4. package/README.src.md +35 -1
  5. package/compatibility-lock.json +3 -3
  6. package/dist/agents/browser.d.ts +12 -0
  7. package/dist/agents/browser.js +12 -0
  8. package/dist/agents/index.d.ts +7 -7
  9. package/dist/agents/index.js +7 -8
  10. package/dist/agents-cli.d.ts +1 -1
  11. package/dist/agents-cli.js +250 -3
  12. package/dist/agents-deploy.d.ts +186 -0
  13. package/dist/agents-deploy.js +752 -0
  14. package/dist/agents-file.d.ts +19 -0
  15. package/dist/agents-file.js +135 -0
  16. package/dist/apps/index.d.ts +10 -0
  17. package/dist/apps/index.js +11 -0
  18. package/dist/apps-cli.d.ts +3 -1
  19. package/dist/apps-cli.js +285 -38
  20. package/dist/blaxel/errors.js +2 -1
  21. package/dist/ci/index.d.ts +10 -0
  22. package/dist/ci/index.js +11 -0
  23. package/dist/ci-cli.d.ts +10 -0
  24. package/dist/ci-cli.js +343 -0
  25. package/dist/cli.d.ts +18 -7
  26. package/dist/cli.js +775 -85
  27. package/dist/daytona/errors.js +2 -1
  28. package/dist/e2b/errors.js +2 -1
  29. package/dist/e2b/filesystem.d.ts +1 -0
  30. package/dist/e2b/filesystem.js +1 -1
  31. package/dist/e2b/sandbox.d.ts +11 -2
  32. package/dist/e2b/sandbox.js +22 -6
  33. package/dist/errors.d.ts +11 -4
  34. package/dist/errors.js +27 -4
  35. package/dist/fly/index.d.ts +102 -0
  36. package/dist/fly/index.js +930 -0
  37. package/dist/index.d.ts +7 -3
  38. package/dist/machine.d.ts +10 -6
  39. package/dist/machine.js +14 -6
  40. package/dist/modal/index.d.ts +34 -0
  41. package/dist/modal/index.js +128 -22
  42. package/dist/products/agents.d.ts +101 -3
  43. package/dist/products/agents.js +49 -0
  44. package/dist/products/apps.d.ts +206 -10
  45. package/dist/products/apps.js +172 -1
  46. package/dist/products/ci.d.ts +332 -0
  47. package/dist/products/ci.js +101 -0
  48. package/dist/products/index.d.ts +8 -0
  49. package/dist/products/index.js +15 -0
  50. package/dist/products/jobs.d.ts +105 -5
  51. package/dist/products/jobs.js +47 -3
  52. package/dist/products/network.d.ts +13 -0
  53. package/dist/products/observability.d.ts +1 -1
  54. package/dist/products/plans.d.ts +131 -0
  55. package/dist/products/plans.js +24 -0
  56. package/dist/products/reservations.d.ts +108 -0
  57. package/dist/products/reservations.js +41 -0
  58. package/dist/products/resources.d.ts +2 -1
  59. package/dist/products/secrets.d.ts +90 -6
  60. package/dist/products/secrets.js +8 -2
  61. package/dist/proxy.d.ts +3 -3
  62. package/dist/proxy.js +20 -0
  63. package/dist/sandbox.d.ts +29 -1
  64. package/dist/sandbox.js +56 -3
  65. package/dist/snapshots.d.ts +1 -1
  66. package/dist/switched-off.d.ts +5 -0
  67. package/dist/switched-off.js +8 -0
  68. package/dist/tar.d.ts +1 -1
  69. package/dist/tar.js +19 -1
  70. package/dist/transport.d.ts +1 -1
  71. package/dist/transport.js +48 -9
  72. package/dist/types.d.ts +35 -4
  73. package/dist/vercel/errors.d.ts +1 -1
  74. package/dist/vercel/errors.js +2 -1
  75. package/dist/vercel/snapshot.js +4 -2
  76. package/package.json +23 -6
package/dist/index.d.ts CHANGED
@@ -14,16 +14,18 @@ export { Files, Process, Processes, SandboxSessions, Terminal, type CreatedSandb
14
14
  export { Tunnel, TunnelStream, type PortForward } from "./tunnel.js";
15
15
  export { Page } from "./page.js";
16
16
  export type { AccountLimit, KeyLimits } from "./products/limits.js";
17
+ export type { BookRoom, RoomBooking, RoomBookingPage } from "./products/reservations.js";
17
18
  export type { AuditEvent, AuditPage } from "./products/audit.js";
19
+ export type { Plan, PlanAddOn, PlanChange, PlanIncluded, PlanLimit, PlanNow, Plans, } from "./products/plans.js";
18
20
  export type { UsageExportQuery, UsageExportRow, UsageExportPage, } from "./products/observability.js";
19
- export type { JobSecret, Secret, SecretRule, SecretScope, SecretUse, SetSecret, } from "./products/secrets.js";
21
+ export type { JobSecret, Secret, SecretApiPolicy, SecretGit, SecretGithubApp, SecretRule, SecretScope, SecretUse, SetSecret, } from "./products/secrets.js";
20
22
  export { SecretPartlyStoredError } from "./products/secrets.js";
21
23
  export type { McpCatalogEntry, McpGateway, McpServerRequest } from "./products/mcp.js";
22
24
  export type { FileEvent, FileEventType, WatchHandle, WatchNotice, WatchOptions, } from "./products/watch.js";
23
25
  export type { Mount, MountBucket } from "./products/mounts.js";
24
26
  export type { TailscaleJoin, TailscaleJoined, TailscaleNode, TailscaleStatus, } from "./products/tailscale.js";
25
27
  export type { Volume, VolumeBackup, CreateVolume, VolumeAttachment } from "./products/volumes.js";
26
- export type { CreateJob, Job, JobCompute, JobDefinition, JobLogs, JobRun, JobRunState, JobSchedule, } from "./products/jobs.js";
28
+ export type { CreateJob, Job, JobAlert, JobAlertDelivery, JobCompute, JobDefinition, JobLogs, JobRun, JobRunState, JobSchedule, UpdateJob, } from "./products/jobs.js";
27
29
  export type { CheckedDomain, DnsRecord, Domain, EgressAddress, TcpPort, Tunnel as PrivateTunnel, TunnelPeer, TunnelPeerCreated, SetUpstreamProxy, UpstreamProxy, } from "./products/network-products.js";
28
30
  export type { PrivateNetwork } from "./products/private-network.js";
29
31
  export type { SignedRequests } from "./products/signed-requests.js";
@@ -76,6 +78,8 @@ export { openWalletAccount, claimWalletAccount, rekeyWalletAccount, type OpenedW
76
78
  export type { Topup } from "./products/billing.js";
77
79
  export type { NewKey, LimitedKey, LimitedKeyAction, MadeKey, ListedKey, KeyUsage, } from "./products/api-keys.js";
78
80
  export type { SsoConnection, SsoStatus } from "./products/sso.js";
79
- export type { Agent, AgentDefinition, AgentEvent, AgentSession, AgentSessionHandle, AgentSessionState, AgentTemplate, AgentTemplateKey, AgentTrigger, AgentTriggerKind, AgentUse, CreateAgent, CreateAgentTrigger, StartAgentSession, } from "./products/agents.js";
81
+ export type { Agent, AgentDefinition, AgentEvent, AgentMemoryEntry, AgentMemoryFile, AgentMemoryHit, AgentMemoryPage, AgentMemoryUsage, AgentMemoryWritten, AgentSession, AgentSessionHandle, AgentSessionState, AgentTemplate, AgentTemplateKey, AgentTrigger, AgentTriggerKind, AgentUse, CreateAgent, CreateAgentTrigger, StartAgentSession, } from "./products/agents.js";
80
82
  export { AGENT_TEMPLATES, AGENT_USES } from "./products/agents.js";
83
+ export type { AgentsDeployChange, AgentsDeployOptions, AgentsDeployResult, AgentsFile, AgentsFileAgent, AgentsFileTrigger, AgentsSource, } from "./agents-deploy.js";
81
84
  export type { Container, ContainerState, CreateContainer, UpdateContainer, } from "./products/containers.js";
85
+ export type { CiFlake, CiFlakeDetail, CiRepository, CiTestResult, ConnectCiRepository, ListCiFlakes, ListCiRepositories, UpdateCiFlake, } from "./products/ci.js";
package/dist/machine.d.ts CHANGED
@@ -90,19 +90,23 @@ export declare abstract class Machine<Info extends MachineInfo, Settings extends
90
90
  * programs stop (snapshot it first to keep its memory too). A running sandbox is paused, or stopped if it is
91
91
  * persistent, and comes back running at the new size on the same server; a
92
92
  * paused or stopped one is started. Memory bills on the new size from then.
93
+ * A larger disk alone on a running sandbox grows live instead, every
94
+ * program left running; `restart: false` asks for that or a refusal
95
+ * (`resize_needs_restart`: paused, stopped, a custom image, or a server
96
+ * that cannot), never a restart.
93
97
  * Refused, with nothing changed, when the server has no room
94
98
  * (`no_capacity`), above your quota or the 2 vCPU and 4 GiB without credit, or
95
- * while a snapshot or fork of it is being taken. `restart` is optional:
96
- * true is the only value, and it is sent either way, so an API from before
97
- * it was optional accepts the call too. */
99
+ * while a snapshot or fork of it is being taken. `restart` defaults to
100
+ * true and is sent either way, so an API from before it was optional
101
+ * accepts the call too. */
98
102
  resize(
99
- /** diskMiB only grows; the filesystem fills it at the restart. */
103
+ /** diskMiB only grows; the filesystem fills it at once (live) or at the restart. */
100
104
  size: {
101
105
  vcpu?: number;
102
106
  memoryMiB?: number;
103
107
  diskMiB?: number;
104
108
  }, options?: RequestOptions & {
105
- restart?: true;
109
+ restart?: boolean;
106
110
  }): Promise<this>;
107
111
  /** Keeps a sandbox with a time limit running past it, in the background,
108
112
  * until stop() or the returned function ends it: every `everySeconds` (60)
@@ -110,7 +114,7 @@ export declare abstract class Machine<Info extends MachineInfo, Settings extends
110
114
  * than the hour ahead the API allows. A sandbox with no time limit
111
115
  * (`endsAt` null) needs none, and is only watched. Running time is billed
112
116
  * as it is used. A paused sandbox is left paused (a request wakes it,
113
- * unless autoWake is off); a stopped one ends the loop. It does not keep a
117
+ * unless autoWake is off); a stopped or deleted one ends the loop. It does not keep a
114
118
  * Node or Bun process alive by itself. */
115
119
  keepAlive(options?: KeepAliveOptions): () => void;
116
120
  /** Days a paused sandbox is kept before deletion (1 to 365), or `null` to
package/dist/machine.js CHANGED
@@ -170,20 +170,24 @@ export class Machine {
170
170
  * programs stop (snapshot it first to keep its memory too). A running sandbox is paused, or stopped if it is
171
171
  * persistent, and comes back running at the new size on the same server; a
172
172
  * paused or stopped one is started. Memory bills on the new size from then.
173
+ * A larger disk alone on a running sandbox grows live instead, every
174
+ * program left running; `restart: false` asks for that or a refusal
175
+ * (`resize_needs_restart`: paused, stopped, a custom image, or a server
176
+ * that cannot), never a restart.
173
177
  * Refused, with nothing changed, when the server has no room
174
178
  * (`no_capacity`), above your quota or the 2 vCPU and 4 GiB without credit, or
175
- * while a snapshot or fork of it is being taken. `restart` is optional:
176
- * true is the only value, and it is sent either way, so an API from before
177
- * it was optional accepts the call too. */
179
+ * while a snapshot or fork of it is being taken. `restart` defaults to
180
+ * true and is sent either way, so an API from before it was optional
181
+ * accepts the call too. */
178
182
  async resize(
179
- /** diskMiB only grows; the filesystem fills it at the restart. */
183
+ /** diskMiB only grows; the filesystem fills it at once (live) or at the restart. */
180
184
  size, options = {}) {
181
185
  const { restart = true, ...rest } = options;
182
186
  this.#keep(await this.#t.json({
183
187
  method: "POST",
184
188
  path: this.#path(":resize"),
185
189
  body: { ...size, restart },
186
- // A pause or stop and a cold boot.
190
+ // A pause or stop and a cold boot (a live grow answers at once).
187
191
  wait: 120,
188
192
  ...requestOnly(rest),
189
193
  }));
@@ -195,7 +199,7 @@ export class Machine {
195
199
  * than the hour ahead the API allows. A sandbox with no time limit
196
200
  * (`endsAt` null) needs none, and is only watched. Running time is billed
197
201
  * as it is used. A paused sandbox is left paused (a request wakes it,
198
- * unless autoWake is off); a stopped one ends the loop. It does not keep a
202
+ * unless autoWake is off); a stopped or deleted one ends the loop. It does not keep a
199
203
  * Node or Bun process alive by itself. */
200
204
  keepAlive(options = {}) {
201
205
  this.#keepAlive?.();
@@ -226,6 +230,10 @@ export class Machine {
226
230
  }
227
231
  }
228
232
  catch (error) {
233
+ // Gone (deleted elsewhere, or by its own time limit): nothing to keep
234
+ // alive, and asking again would only be refused.
235
+ if (error instanceof RuntimeError && error.status === 404)
236
+ return end();
229
237
  options.onError?.(error);
230
238
  }
231
239
  if (ended)
@@ -149,6 +149,21 @@ export declare class Proxy {
149
149
  export declare class ProxyService {
150
150
  readonly fromName: RefusedAsync;
151
151
  }
152
+ export declare class FunctionService {
153
+ readonly fromName: RefusedAsync;
154
+ readonly fromId: RefusedAsync;
155
+ }
156
+ export declare class FunctionCallService {
157
+ readonly fromId: RefusedAsync;
158
+ }
159
+ export declare class ClsService {
160
+ readonly fromName: RefusedAsync;
161
+ }
162
+ export declare class QueueService {
163
+ readonly fromName: RefusedAsync;
164
+ readonly ephemeral: RefusedAsync;
165
+ readonly delete: RefusedAsync;
166
+ }
152
167
  export declare class ExperimentalOutboundPolicy {
153
168
  readonly withHeaderReplacement: Refused;
154
169
  }
@@ -185,8 +200,14 @@ export declare class Image {
185
200
  /** The id is a native disk snapshot (snapshotFilesystem), not an image. */
186
201
  snapshot?: boolean);
187
202
  get imageId(): string;
203
+ /** A layer of Dockerfile commands. Its `secrets` refuse: a Runtime build
204
+ * keeps its build arguments in the image's build plan, history and cache
205
+ * key, and has no channel that never keeps a value, so a secret is used at
206
+ * run time instead (security review, 18 October 2026). */
188
207
  dockerfileCommands(commands: string[], params?: {
189
208
  env?: Record<string, string>;
209
+ secrets?: Secret[];
210
+ gpu?: string;
190
211
  forceBuild?: boolean;
191
212
  }): Image;
192
213
  build(_app?: App): Promise<Image>;
@@ -248,6 +269,10 @@ export declare class ModalClient {
248
269
  readonly sandboxSnapshots: SandboxSnapshotService;
249
270
  readonly proxies: ProxyService;
250
271
  readonly environments: EnvironmentService;
272
+ readonly functions: FunctionService;
273
+ readonly functionCalls: FunctionCallService;
274
+ readonly cls: ClsService;
275
+ readonly queues: QueueService;
251
276
  constructor(options?: ModalClientParams);
252
277
  /** Throws once close() was called, as Modal's client does. */
253
278
  open(): void;
@@ -269,6 +294,7 @@ export type SandboxCreateParams = {
269
294
  memoryLimitMiB?: number;
270
295
  runtime?: "gvisor" | "vm";
271
296
  timeoutMs?: number;
297
+ idleTimeoutMs?: number;
272
298
  workdir?: string;
273
299
  command?: string[];
274
300
  env?: Record<string, string>;
@@ -285,6 +311,7 @@ export type SandboxCreateParams = {
285
311
  cloudBucketMounts?: Record<string, CloudBucketMount>;
286
312
  readinessProbe?: Probe;
287
313
  experimentalEnableSnapshot?: boolean;
314
+ experimentalOptions?: Record<string, boolean | string>;
288
315
  };
289
316
  export type SandboxExecParams = {
290
317
  mode?: "text" | "binary";
@@ -341,6 +368,7 @@ export declare class ModalWriteStream<R extends string | Uint8Array = string> ex
341
368
  writeBytes(bytes: Uint8Array): Promise<void>;
342
369
  }
343
370
  export declare class ContainerProcess<R extends string | Uint8Array = string> {
371
+ #private;
344
372
  readonly native: Process;
345
373
  readonly stdin: ModalWriteStream<R>;
346
374
  readonly stdout: ModalReadStream<R>;
@@ -348,6 +376,8 @@ export declare class ContainerProcess<R extends string | Uint8Array = string> {
348
376
  constructor(native: Process, mode: "text" | "binary", params?: SandboxExecParams);
349
377
  wait(): Promise<number>;
350
378
  closeStdin(): Promise<void>;
379
+ /** null while it runs, then its exit code. A process whose sandbox ended
380
+ * before its exit was seen was killed with it: 137, as Modal reports. */
351
381
  _poll(): Promise<number | null>;
352
382
  }
353
383
  /** Host and port of a sandbox port's public HTTPS address. Runtime previews
@@ -400,6 +430,10 @@ export declare class Sandbox {
400
430
  monitorEntrypoint(): void;
401
431
  constructor(native: NativeSandbox, primary: ContainerProcess, client?: ModalClient | undefined);
402
432
  get sandboxId(): string;
433
+ /** Ends the sandbox after `ms`, for a Modal timeout under Runtime's
434
+ * shortest time limit (a minute, which stays the bound if this client
435
+ * goes away). The timer never keeps the program running. */
436
+ terminateAfter(ms: number): void;
403
437
  private attached;
404
438
  exec(command: string[], params?: SandboxExecParams & {
405
439
  mode?: "text";
@@ -274,6 +274,26 @@ export class ProxyService {
274
274
  const STATIC_IPS = "Runtime static egress IPs: runtime.ips";
275
275
  refuse("Modal", "Proxy", Proxy, { proxyId: STATIC_IPS }, { sync: true });
276
276
  refuse("Modal", "ProxyService", ProxyService, { fromName: STATIC_IPS });
277
+ // Modal's other products, outside its sandboxes: each call refuses, naming
278
+ // what to use, where it was undefined before (function-call.ts, 18 October 2026).
279
+ const FUNCTIONS = "Runtime runs sandboxes, not Modal Functions: run the code in a sandbox (modal.sandboxes.create), or on a schedule as a Runtime job (runtime.jobs)";
280
+ const QUEUES = "keep the queue in your own store, or in a file on a Runtime volume (runtime.volumes)";
281
+ export class FunctionService {
282
+ }
283
+ export class FunctionCallService {
284
+ }
285
+ export class ClsService {
286
+ }
287
+ export class QueueService {
288
+ }
289
+ refuse("Modal", "FunctionService", FunctionService, { fromName: FUNCTIONS, fromId: FUNCTIONS });
290
+ refuse("Modal", "FunctionCallService", FunctionCallService, { fromId: FUNCTIONS });
291
+ refuse("Modal", "ClsService", ClsService, { fromName: FUNCTIONS });
292
+ refuse("Modal", "QueueService", QueueService, {
293
+ fromName: QUEUES,
294
+ ephemeral: QUEUES,
295
+ delete: QUEUES,
296
+ });
277
297
  export class ExperimentalOutboundPolicy {
278
298
  }
279
299
  refuse("Modal", "ExperimentalOutboundPolicy", ExperimentalOutboundPolicy, {
@@ -309,6 +329,7 @@ export class Probe {
309
329
  }
310
330
  refuse("Modal", "Probe", Probe, { toProto: "pass it as readinessProbe" }, { sync: true });
311
331
  // Images ------------------------------------------------------------------------
332
+ const BUILD_SECRETS = "a Runtime image build keeps no secret: use it at run time, with sandboxes.create(app, image, { secrets }), or runtime.secrets.set({ value, hosts }) so the sandbox never sees it";
312
333
  export class Image {
313
334
  runtime;
314
335
  tag;
@@ -335,10 +356,18 @@ export class Image {
335
356
  throw new Error("Image is not built; await image.build(app) first.");
336
357
  return this.built;
337
358
  }
359
+ /** A layer of Dockerfile commands. Its `secrets` refuse: a Runtime build
360
+ * keeps its build arguments in the image's build plan, history and cache
361
+ * key, and has no channel that never keeps a value, so a secret is used at
362
+ * run time instead (security review, 18 October 2026). */
338
363
  dockerfileCommands(commands, params = {}) {
339
- only("Modal image", params, ["env", "forceBuild"]);
364
+ only("Modal image", params, ["env", "secrets", "gpu", "forceBuild"]);
365
+ if (params.gpu)
366
+ throw new CompatibilityError("Modal", "GPU image builds");
340
367
  if (!this.tag)
341
368
  throw new CompatibilityError("Modal", "extending a prebuilt Runtime image through Dockerfile layers");
369
+ if (params.secrets?.length)
370
+ throw refusal("Modal", "Image.dockerfileCommands secrets", BUILD_SECRETS);
342
371
  return new Image(this.runtime, this.tag, undefined, [...this.commands, ...commands], { ...this.env, ...params.env }, params.forceBuild ? false : this.cache);
343
372
  }
344
373
  async build(_app) {
@@ -433,6 +462,10 @@ export class ModalClient {
433
462
  sandboxSnapshots;
434
463
  proxies = new ProxyService();
435
464
  environments = new EnvironmentService();
465
+ functions = new FunctionService();
466
+ functionCalls = new FunctionCallService();
467
+ cls = new ClsService();
468
+ queues = new QueueService();
436
469
  #closed = false;
437
470
  constructor(options = {}) {
438
471
  if (options.endpoint !== undefined ||
@@ -496,21 +529,45 @@ refuse("Modal", "ModalClient", ModalClient, {
496
529
  refuse("Modal", "ModalClient", ModalClient, {
497
530
  getImageBuilderVersion: "Runtime images record theirs: (await runtime.images.get(id)).build.builderVersion",
498
531
  });
499
- /** Modal's outbound allowlists as Runtime network rules, never wider than
500
- * Modal's: a missing list or a wildcard entry allows everything, an empty
501
- * list blocks all egress. Runtime's rules are one list a connection matches
502
- * any entry of, so a domain list and a CIDR list that both narrow are
503
- * refused rather than joined into a wider rule. */
532
+ // Sandboxes ---------------------------------------------------------------------
533
+ /** Who a sandbox's processes run as: root, as in a Modal container (and a
534
+ * registry image's default user), where Runtime's own default is uid 1000.
535
+ * Modal code writes to /, installs packages and starts daemons unprefixed. */
536
+ const ROOT = "root";
537
+ /** Modal's outbound allowlists as Runtime network rules. Modal allows a
538
+ * connection a listed domain or a listed CIDR range admits, and a list left
539
+ * out admits nothing once the other is given (modal-client
540
+ * buildOutboundNetworkAccess; its guide: "except traffic allowed by
541
+ * outbound_domain_allowlist"). Runtime's `allow` is the same union, one list
542
+ * a connection matches any entry of. Modal's `*.example.com` also matches
543
+ * example.com, where Runtime's leaves the apex out, so both are listed.
544
+ * Every domain ("*") with narrower addresses is refused: Runtime cannot
545
+ * admit every name without every address. Before 18 October 2026 an empty
546
+ * CIDR list blocked the listed domains too, and two narrowing lists were
547
+ * refused (Modal's sandbox-network-policy.ts gives both). */
504
548
  function egress(domains, cidrs) {
505
- if (domains?.length === 0 || cidrs?.length === 0)
506
- return { internet: false };
507
- const anyDomain = !domains || domains.includes("*");
508
- const anyCidr = !cidrs || cidrs.some((c) => c === "0.0.0.0/0" || c === "::/0");
509
- if (anyDomain && anyCidr)
549
+ if (domains === undefined && cidrs === undefined)
550
+ return { internet: true };
551
+ // Every address is both families' whole range. One of them alone stays an
552
+ // ordinary rule: ::/0 by itself opened IPv4 as well, wider than Modal.
553
+ const whole = [...new Set(cidrs ?? [])].filter((c) => c === "0.0.0.0/0" || c === "::/0");
554
+ if (whole.length === 2)
510
555
  return { internet: true };
511
- if (!anyDomain && !anyCidr)
512
- throw new CompatibilityError("Modal", "a domain allowlist and a CIDR allowlist at once; pass one list of domains and CIDR ranges to sandbox.native.network.set({ internet: true, allow })");
513
- return { internet: true, allow: anyDomain ? cidrs : domains };
556
+ if ((domains ?? []).includes("*")) {
557
+ // Every name beside one family's whole range: Runtime has no rule for
558
+ // every name, so the range alone (Modal's own examples pass "*" with
559
+ // 0.0.0.0/0). Narrower than Modal's by the other family, never wider.
560
+ if (whole.length)
561
+ return { internet: true, allow: whole };
562
+ throw new CompatibilityError("Modal", 'a domain allowlist of "*" with a narrower CIDR allowlist; pass ["0.0.0.0/0", "::/0"] with it for all traffic, or list the domains');
563
+ }
564
+ const allow = [
565
+ ...new Set([
566
+ ...(domains ?? []).flatMap((d) => (d.startsWith("*.") ? [d, d.slice(2)] : [d])),
567
+ ...(cidrs ?? []),
568
+ ]),
569
+ ];
570
+ return allow.length ? { internet: true, allow } : { internet: false };
514
571
  }
515
572
  const ENVIRONMENTS = "named Modal environments";
516
573
  const CREATE_OPTIONS = [
@@ -521,6 +578,7 @@ const CREATE_OPTIONS = [
521
578
  "memoryLimitMiB",
522
579
  "runtime",
523
580
  "timeoutMs",
581
+ "idleTimeoutMs",
524
582
  "workdir",
525
583
  "command",
526
584
  "env",
@@ -537,6 +595,7 @@ const CREATE_OPTIONS = [
537
595
  "cloudBucketMounts",
538
596
  "readinessProbe",
539
597
  "experimentalEnableSnapshot",
598
+ "experimentalOptions",
540
599
  ];
541
600
  export class SandboxService {
542
601
  runtime;
@@ -566,6 +625,15 @@ export class SandboxService {
566
625
  throw new TypeError("blockNetwork cannot be combined with allowlists");
567
626
  if (params.runtime !== undefined && params.runtime !== "vm")
568
627
  throw new InvalidError(`Unknown sandbox runtime ${String(params.runtime)}`);
628
+ for (const [option, value] of Object.entries(params.experimentalOptions ?? {}))
629
+ throw option === "enable_exit_snapshot" && value
630
+ ? refusal("Modal", "experimentalOptions.enable_exit_snapshot", "call sandbox.snapshotFilesystem() before terminate(), then start new sandboxes from that Image")
631
+ : new CompatibilityError("Modal", `experimental option ${option}`);
632
+ if (params.timeoutMs !== undefined && !(params.timeoutMs > 0))
633
+ throw new InvalidError(`timeoutMs must be positive, got ${params.timeoutMs}`);
634
+ if (params.idleTimeoutMs !== undefined &&
635
+ (!(params.idleTimeoutMs > 0) || params.idleTimeoutMs % 1000 !== 0))
636
+ throw new InvalidError(`idleTimeoutMs must be a positive multiple of 1000ms, got ${params.idleTimeoutMs}`);
569
637
  const network = params.blockNetwork
570
638
  ? { internet: false }
571
639
  : egress(params.outboundDomainAllowlist, params.outboundCidrAllowlist);
@@ -604,11 +672,22 @@ export class SandboxService {
604
672
  region: placement,
605
673
  ...(volumes.length ? { volumes } : {}),
606
674
  // A timeout is the customer's limit, and the sandbox never pauses
607
- // for idleness before it. None is no time limit (0300): it runs while
608
- // it works and pauses when idle.
675
+ // for idleness before it unless idleTimeoutMs asks. None is no time
676
+ // limit (0300): it runs while it works and pauses when idle. Runtime's
677
+ // shortest limit is a minute: a shorter one is kept by this client
678
+ // (below), with the minute as the bound if the client goes away.
609
679
  ...(params.timeoutMs === undefined
610
680
  ? {}
611
- : { timeoutSeconds: Math.ceil(params.timeoutMs / 1000), idlePauseSeconds: 0 }),
681
+ : {
682
+ timeoutSeconds: Math.max(60, Math.ceil(params.timeoutMs / 1000)),
683
+ idlePauseSeconds: 0,
684
+ }),
685
+ // Modal ends an idle sandbox; Runtime pauses it, memory kept and CPU
686
+ // no longer billed, and wakes it at the next command, file call or
687
+ // tunnel request. Runtime's shortest idle pause is ten seconds.
688
+ ...(params.idleTimeoutMs === undefined
689
+ ? {}
690
+ : { idlePauseSeconds: Math.max(10, params.idleTimeoutMs / 1000) }),
612
691
  onLeaseEnd: "stop",
613
692
  network,
614
693
  };
@@ -616,7 +695,7 @@ export class SandboxService {
616
695
  for (const mount of mounts)
617
696
  await s.mounts.add(mount);
618
697
  primary = await s.spawn(params.command ?? ["sleep", "infinity"], {
619
- ...(await execOptions(s, { cwd: params.workdir })),
698
+ ...(await execOptions(s, { cwd: params.workdir, user: ROOT })),
620
699
  stdin: "pipe",
621
700
  pty: params.pty ? {} : undefined,
622
701
  });
@@ -626,6 +705,8 @@ export class SandboxService {
626
705
  }));
627
706
  const sandbox = new Sandbox(s, new ContainerProcess(primary, "text"), this.client);
628
707
  sandbox.monitorEntrypoint();
708
+ if (params.timeoutMs !== undefined && params.timeoutMs < 60_000)
709
+ sandbox.terminateAfter(params.timeoutMs);
629
710
  return sandbox;
630
711
  }
631
712
  async experimentalCreate(app, image, params = {}) {
@@ -807,18 +888,35 @@ export class ContainerProcess {
807
888
  this.stdout = new ModalReadStream(native, "stdout", mode, params.stdout === "ignore");
808
889
  this.stderr = new ModalReadStream(native, "stderr", mode, params.stderr === "ignore");
809
890
  }
891
+ /** The exit code, once seen: a poll after the sandbox ended still has it. */
892
+ #exitCode;
810
893
  async wait() {
811
894
  const r = await this.native.wait();
812
895
  if (r.timedOut)
813
896
  throw new SandboxTimeoutError("Process timed out");
814
- return r.exitCode ?? -1;
897
+ this.#exitCode = r.exitCode ?? -1;
898
+ return this.#exitCode;
815
899
  }
816
900
  closeStdin() {
817
901
  return this.native.write("", { eof: true });
818
902
  }
903
+ /** null while it runs, then its exit code. A process whose sandbox ended
904
+ * before its exit was seen was killed with it: 137, as Modal reports. */
819
905
  async _poll() {
820
- const p = await this.native.refresh();
821
- return p.state === "running" ? null : p.exitCode;
906
+ if (this.#exitCode !== undefined)
907
+ return this.#exitCode;
908
+ try {
909
+ const p = await this.native.refresh();
910
+ if (p.state === "running")
911
+ return null;
912
+ return (this.#exitCode = p.exitCode ?? -1);
913
+ }
914
+ catch (error) {
915
+ if (error instanceof RuntimeError &&
916
+ (error.code === "sandbox_stopped" || error.status === 404))
917
+ return (this.#exitCode = 137);
918
+ throw error;
919
+ }
822
920
  }
823
921
  }
824
922
  /** Host and port of a sandbox port's public HTTPS address. Runtime previews
@@ -903,6 +1001,13 @@ export class Sandbox {
903
1001
  get sandboxId() {
904
1002
  return this.native.id;
905
1003
  }
1004
+ /** Ends the sandbox after `ms`, for a Modal timeout under Runtime's
1005
+ * shortest time limit (a minute, which stays the bound if this client
1006
+ * goes away). The timer never keeps the program running. */
1007
+ terminateAfter(ms) {
1008
+ const timer = setTimeout(() => void this.terminate().catch(() => undefined), ms);
1009
+ timer.unref?.();
1010
+ }
906
1011
  attached() {
907
1012
  this.client?.open();
908
1013
  if (this.detached)
@@ -922,6 +1027,7 @@ export class Sandbox {
922
1027
  ]);
923
1028
  const p = await this.native.spawn(command, {
924
1029
  ...(await execOptions(this.native, {
1030
+ user: ROOT,
925
1031
  cwd: params.workdir,
926
1032
  timeoutMs: params.timeoutMs === 0 ? 86_400_000 : params.timeoutMs,
927
1033
  env: Object.assign({}, ...(params.secrets?.map((s) => s.values) ?? []), params.env ?? {}),
@@ -989,7 +1095,7 @@ export class Sandbox {
989
1095
  const left = Math.ceil(until - performance.now());
990
1096
  if (left <= 0)
991
1097
  throw new TimeoutError("Timeout expired");
992
- const result = await this.native.exec(argv, { timeoutMs: left });
1098
+ const result = await this.native.exec(argv, { timeoutMs: left, user: ROOT });
993
1099
  if (result.exitCode === 0)
994
1100
  return;
995
1101
  await new Promise((resolve) => setTimeout(resolve, Math.min(probe.intervalMs, left)));
@@ -23,13 +23,19 @@ import type { RequestOptions, Transport } from "../transport.js";
23
23
  export type AgentDefinition = {
24
24
  /** What runs the harness, from the sandbox's home. */
25
25
  command: string;
26
- /** Where each session's sandbox starts: an image of yours, or a snapshot
27
- * of a machine you set up by hand (a harness logged in to your own
28
- * subscription included). The default image has Node and Python. */
26
+ /** Where each session's sandbox starts: an image of yours; a snapshot of
27
+ * a machine you set up by hand (a harness logged in to your own
28
+ * subscription included); or a GitHub repository connected to CI/CD,
29
+ * `owner/name`, whose warm snapshot (checked out at /workspace/repo at its
30
+ * production branch's last passing commit, set up) each session starts
31
+ * as a copy of, its harness run there. The default image has Node and
32
+ * Python. */
29
33
  from?: {
30
34
  image: string;
31
35
  } | {
32
36
  snapshot: string;
37
+ } | {
38
+ repo: string;
33
39
  };
34
40
  /** 1, 2, 4, 8 or 16; default 4. */
35
41
  vcpu?: number;
@@ -61,8 +67,19 @@ export type AgentDefinition = {
61
67
  maxSessionSeconds?: number;
62
68
  /** Sessions running at once; more wait, queued. Default 10. */
63
69
  maxConcurrent?: number;
70
+ /** What one session may cost, its subagents' sessions included, in
71
+ * dollars; it stops with reason `budget` there. Default 5. */
72
+ maxSessionDollars?: number;
73
+ /** What the agent's sessions may cost in a day, in dollars; past it the
74
+ * agent starts nothing new (`budget_reached`) until the next. Default 100. */
75
+ maxDayDollars?: number;
64
76
  /** The use it was set up for (`uses`). */
65
77
  template?: "support" | "team";
78
+ /** Whose memory a session reads: `key` (the default), each session key
79
+ * its own; `agent`, one all its sessions share, never a support agent's. */
80
+ memory?: {
81
+ scope: "key" | "agent";
82
+ };
66
83
  };
67
84
  export type Agent = {
68
85
  id: string;
@@ -122,6 +139,8 @@ export type AgentTrigger = {
122
139
  pausedAt: string | null;
123
140
  pausedWhy: string | null;
124
141
  createdAt: string;
142
+ /** When it last changed: made, paused or resumed. */
143
+ updatedAt: string;
125
144
  /** A webhook's secret, which each of its calls carries: answered once,
126
145
  * when it is added, and never again. Keep it where you keep your keys. */
127
146
  secret?: string;
@@ -164,6 +183,50 @@ export type CreateAgentTrigger = {
164
183
  };
165
184
  /** The five harnesses' names, so `templates["claude-code"]` is known to be there. */
166
185
  export type AgentTemplateKey = "claude-code" | "codex" | "gemini-cli" | "claude-agent-sdk" | "openai-agents-sdk";
186
+ /** A file of an agent's memory (`memory.list`). `scope` is whose: empty for
187
+ * the shared memory, which every session reads (one keeping memory per key
188
+ * reads what you wrote there, and never changes it), or a session key
189
+ * (`customer:42`) for that key's alone. */
190
+ export type AgentMemoryEntry = {
191
+ scope: string;
192
+ path: string;
193
+ /** Its text and path, in bytes, as billed. */
194
+ bytes: number;
195
+ updatedAt: string;
196
+ /** A key's principal id, or session:<id> when the agent wrote it. */
197
+ updatedBy: string;
198
+ };
199
+ export type AgentMemoryFile = AgentMemoryEntry & {
200
+ content: string;
201
+ };
202
+ export type AgentMemoryHit = {
203
+ scope: string;
204
+ path: string;
205
+ /** Its words around the match, matches in **bold**. */
206
+ snippet: string;
207
+ /** Higher is better. */
208
+ rank: number;
209
+ bytes: number;
210
+ updatedAt: string;
211
+ };
212
+ export type AgentMemoryWritten = {
213
+ scope: string;
214
+ path: string;
215
+ bytes: number;
216
+ /** True for a new file, false for one replaced. */
217
+ created: boolean;
218
+ updatedAt: string;
219
+ };
220
+ /** The agent's whole memory, every scope: what storage is billed on. */
221
+ export type AgentMemoryUsage = {
222
+ bytes: number;
223
+ files: number;
224
+ limitBytes: number;
225
+ limitFiles: number;
226
+ };
227
+ export type AgentMemoryPage = Page<AgentMemoryEntry> & {
228
+ readonly usage: AgentMemoryUsage;
229
+ };
167
230
  /** A harness, as data (packages/cloud-guide/agent-templates.json, the same
168
231
  * in every SDK and the guide). `definition` is what an agent's definition
169
232
  * takes from it; add a use's (`uses`) and your instructions. */
@@ -197,6 +260,8 @@ export type AgentUse = {
197
260
  /** The five harnesses (agents-contract.md); tests/agents.test.ts holds them
198
261
  * to packages/cloud-guide/agent-templates.json. */
199
262
  export declare const AGENT_TEMPLATES: Readonly<Record<AgentTemplateKey, AgentTemplate>>;
263
+ /** A harness by a key read from a file or a flag, or undefined. */
264
+ export declare function agentTemplate(key: string): AgentTemplate | undefined;
200
265
  export declare const AGENT_USES: Readonly<Record<"support" | "team", AgentUse>>;
201
266
  /** One session, to talk to and follow. */
202
267
  export type AgentSessionHandle = {
@@ -264,6 +329,39 @@ export declare function agents(t: Transport): {
264
329
  resume: (id: string, options?: RequestOptions) => Promise<AgentTrigger>;
265
330
  delete: (id: string, options?: RequestOptions) => Promise<void>;
266
331
  };
332
+ /** Its built-in memory: text files kept across sessions, which the
333
+ * agent reads, writes and searches from inside its sessions. A scope is
334
+ * whose: "" (the default) for what every session reads, or a session key
335
+ * ("customer:42") for that key's alone. Up to 1 MiB a file, 1 GiB and
336
+ * 100,000 files an agent, billed as storage. */
337
+ memory: {
338
+ /** Its files by scope and path, a page at a time, with its usage.
339
+ * Without scope, every scope. */
340
+ list: (agent: string, query?: {
341
+ scope?: string;
342
+ prefix?: string;
343
+ cursor?: string;
344
+ limit?: number;
345
+ }, options?: RequestOptions) => Promise<AgentMemoryPage>;
346
+ /** The files whose words or paths match, best first, each with a
347
+ * snippet: words, "a phrase", or, -not. Without scope, every scope. */
348
+ search: (agent: string, q: string, query?: {
349
+ scope?: string;
350
+ limit?: number;
351
+ }, options?: RequestOptions) => Promise<AgentMemoryHit[]>;
352
+ /** One file, with its text; not_found when there is none. */
353
+ read: (agent: string, path: string, query?: {
354
+ scope?: string;
355
+ }, options?: RequestOptions) => Promise<AgentMemoryFile>;
356
+ /** Seeds or replaces a file. */
357
+ write: (agent: string, path: string, content: string, query?: {
358
+ scope?: string;
359
+ }, options?: RequestOptions) => Promise<AgentMemoryWritten>;
360
+ /** Answers whether there was a file. */
361
+ delete: (agent: string, path: string, query?: {
362
+ scope?: string;
363
+ }, options?: RequestOptions) => Promise<boolean>;
364
+ };
267
365
  /** The harnesses, as data: claude-code, codex, gemini-cli,
268
366
  * claude-agent-sdk, openai-agents-sdk. */
269
367
  templates: Readonly<Record<AgentTemplateKey, AgentTemplate>>;