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.
- package/COMPATIBILITY.md +64 -3
- package/E2B.md +1 -1
- package/README.md +36 -2
- package/README.src.md +35 -1
- package/compatibility-lock.json +3 -3
- package/dist/agents/browser.d.ts +12 -0
- package/dist/agents/browser.js +12 -0
- package/dist/agents/index.d.ts +7 -7
- package/dist/agents/index.js +7 -8
- package/dist/agents-cli.d.ts +1 -1
- package/dist/agents-cli.js +250 -3
- package/dist/agents-deploy.d.ts +186 -0
- package/dist/agents-deploy.js +752 -0
- package/dist/agents-file.d.ts +19 -0
- package/dist/agents-file.js +135 -0
- package/dist/apps/index.d.ts +10 -0
- package/dist/apps/index.js +11 -0
- package/dist/apps-cli.d.ts +3 -1
- package/dist/apps-cli.js +285 -38
- package/dist/blaxel/errors.js +2 -1
- package/dist/ci/index.d.ts +10 -0
- package/dist/ci/index.js +11 -0
- package/dist/ci-cli.d.ts +10 -0
- package/dist/ci-cli.js +343 -0
- package/dist/cli.d.ts +18 -7
- package/dist/cli.js +775 -85
- package/dist/daytona/errors.js +2 -1
- package/dist/e2b/errors.js +2 -1
- package/dist/e2b/filesystem.d.ts +1 -0
- package/dist/e2b/filesystem.js +1 -1
- package/dist/e2b/sandbox.d.ts +11 -2
- package/dist/e2b/sandbox.js +22 -6
- package/dist/errors.d.ts +11 -4
- package/dist/errors.js +27 -4
- package/dist/fly/index.d.ts +102 -0
- package/dist/fly/index.js +930 -0
- package/dist/index.d.ts +7 -3
- package/dist/machine.d.ts +10 -6
- package/dist/machine.js +14 -6
- package/dist/modal/index.d.ts +34 -0
- package/dist/modal/index.js +128 -22
- package/dist/products/agents.d.ts +101 -3
- package/dist/products/agents.js +49 -0
- package/dist/products/apps.d.ts +206 -10
- package/dist/products/apps.js +172 -1
- package/dist/products/ci.d.ts +332 -0
- package/dist/products/ci.js +101 -0
- package/dist/products/index.d.ts +8 -0
- package/dist/products/index.js +15 -0
- package/dist/products/jobs.d.ts +105 -5
- package/dist/products/jobs.js +47 -3
- package/dist/products/network.d.ts +13 -0
- package/dist/products/observability.d.ts +1 -1
- package/dist/products/plans.d.ts +131 -0
- package/dist/products/plans.js +24 -0
- package/dist/products/reservations.d.ts +108 -0
- package/dist/products/reservations.js +41 -0
- package/dist/products/resources.d.ts +2 -1
- package/dist/products/secrets.d.ts +90 -6
- package/dist/products/secrets.js +8 -2
- package/dist/proxy.d.ts +3 -3
- package/dist/proxy.js +20 -0
- package/dist/sandbox.d.ts +29 -1
- package/dist/sandbox.js +56 -3
- package/dist/snapshots.d.ts +1 -1
- package/dist/switched-off.d.ts +5 -0
- package/dist/switched-off.js +8 -0
- package/dist/tar.d.ts +1 -1
- package/dist/tar.js +19 -1
- package/dist/transport.d.ts +1 -1
- package/dist/transport.js +48 -9
- package/dist/types.d.ts +35 -4
- package/dist/vercel/errors.d.ts +1 -1
- package/dist/vercel/errors.js +2 -1
- package/dist/vercel/snapshot.js +4 -2
- 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`
|
|
96
|
-
* true
|
|
97
|
-
*
|
|
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?:
|
|
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`
|
|
176
|
-
* true
|
|
177
|
-
*
|
|
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)
|
package/dist/modal/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/modal/index.js
CHANGED
|
@@ -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
|
-
|
|
500
|
-
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
|
|
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
|
|
506
|
-
return { internet:
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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 (
|
|
512
|
-
|
|
513
|
-
|
|
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
|
|
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
|
-
: {
|
|
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
|
-
|
|
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
|
-
|
|
821
|
-
|
|
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
|
|
27
|
-
*
|
|
28
|
-
* subscription included)
|
|
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>>;
|