|
1 | | -import { ModalClient } from "modal"; |
| 1 | +import { ModalClient, type Image, type SandboxCreateParams } from "modal"; |
2 | 2 | import type { SandboxProvider } from "./types.ts"; |
3 | | -import { DEFAULT_AGENTS, SANDBOX_AGENT_INSTALL_SCRIPT } from "./shared.ts"; |
| 3 | +import { DEFAULT_SANDBOX_AGENT_IMAGE } from "./shared.ts"; |
4 | 4 |
|
5 | 5 | const DEFAULT_AGENT_PORT = 3000; |
6 | 6 | const DEFAULT_APP_NAME = "sandbox-agent"; |
7 | 7 | const DEFAULT_MEMORY_MIB = 2048; |
8 | 8 |
|
| 9 | +type ModalCreateOverrides = Omit<Partial<SandboxCreateParams>, "secrets" | "encryptedPorts"> & { |
| 10 | + secrets?: Record<string, string>; |
| 11 | + encryptedPorts?: number[]; |
| 12 | + appName?: string; |
| 13 | +}; |
| 14 | + |
9 | 15 | export interface ModalProviderOptions { |
10 | | - create?: { |
11 | | - secrets?: Record<string, string>; |
12 | | - appName?: string; |
13 | | - memoryMiB?: number; |
14 | | - }; |
| 16 | + create?: ModalCreateOverrides | (() => ModalCreateOverrides | Promise<ModalCreateOverrides>); |
| 17 | + image?: string | Image; |
15 | 18 | agentPort?: number; |
16 | 19 | } |
17 | 20 |
|
| 21 | +async function resolveCreateOptions(value: ModalProviderOptions["create"]): Promise<ModalCreateOverrides> { |
| 22 | + if (!value) return {}; |
| 23 | + return typeof value === "function" ? await value() : value; |
| 24 | +} |
| 25 | + |
18 | 26 | export function modal(options: ModalProviderOptions = {}): SandboxProvider { |
19 | 27 | const agentPort = options.agentPort ?? DEFAULT_AGENT_PORT; |
20 | | - const appName = options.create?.appName ?? DEFAULT_APP_NAME; |
21 | | - const memoryMiB = options.create?.memoryMiB ?? DEFAULT_MEMORY_MIB; |
22 | 28 | const client = new ModalClient(); |
23 | 29 |
|
24 | 30 | return { |
25 | 31 | name: "modal", |
26 | 32 | async create(): Promise<string> { |
| 33 | + const createOpts = await resolveCreateOptions(options.create); |
| 34 | + const appName = createOpts.appName ?? DEFAULT_APP_NAME; |
| 35 | + const baseImage = options.image ?? DEFAULT_SANDBOX_AGENT_IMAGE; |
27 | 36 | const app = await client.apps.fromName(appName, { createIfMissing: true }); |
28 | 37 |
|
29 | | - // Pre-install sandbox-agent and agents in the image so they are cached |
30 | | - // across sandbox creates and don't need to be installed at runtime. |
31 | | - const installAgentCmds = DEFAULT_AGENTS.map((agent) => `RUN sandbox-agent install-agent ${agent}`); |
32 | | - const image = client.images |
33 | | - .fromRegistry("node:22-slim") |
34 | | - .dockerfileCommands([ |
35 | | - "RUN apt-get update && apt-get install -y curl ca-certificates && rm -rf /var/lib/apt/lists/*", |
36 | | - `RUN curl -fsSL ${SANDBOX_AGENT_INSTALL_SCRIPT} | sh`, |
37 | | - ...installAgentCmds, |
38 | | - ]); |
| 38 | + // The default `-full` base image already includes sandbox-agent and all |
| 39 | + // agents pre-installed, so no additional dockerfile commands are needed. |
| 40 | + const image = typeof baseImage === "string" ? client.images.fromRegistry(baseImage) : baseImage; |
39 | 41 |
|
40 | | - const envVars = options.create?.secrets ?? {}; |
| 42 | + const envVars = createOpts.secrets ?? {}; |
41 | 43 | const secrets = Object.keys(envVars).length > 0 ? [await client.secrets.fromObject(envVars)] : []; |
| 44 | + const sandboxCreateOpts = { ...createOpts }; |
| 45 | + delete sandboxCreateOpts.appName; |
| 46 | + delete sandboxCreateOpts.secrets; |
| 47 | + |
| 48 | + const extraPorts = createOpts.encryptedPorts ?? []; |
| 49 | + delete sandboxCreateOpts.encryptedPorts; |
42 | 50 |
|
43 | 51 | const sb = await client.sandboxes.create(app, image, { |
44 | | - encryptedPorts: [agentPort], |
| 52 | + ...sandboxCreateOpts, |
| 53 | + encryptedPorts: [agentPort, ...extraPorts], |
45 | 54 | secrets, |
46 | | - memoryMiB, |
| 55 | + memoryMiB: sandboxCreateOpts.memoryMiB ?? DEFAULT_MEMORY_MIB, |
47 | 56 | }); |
48 | 57 |
|
49 | 58 | // Start the server as a long-running exec process. We intentionally |
|
0 commit comments