Skip to content

Commit 32dd591

Browse files
authored
Merge pull request #269 from rivet-dev/e2b-base-image-support
feat(providers): add base image support and improve forward compatibility
2 parents f353e39 + fe8fbfc commit 32dd591

10 files changed

Lines changed: 240 additions & 38 deletions

File tree

docs/deploy/computesdk.mdx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@ if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY
2727

2828
const sdk = await SandboxAgent.start({
2929
sandbox: computesdk({
30-
create: { envs },
30+
create: {
31+
envs,
32+
image: process.env.COMPUTESDK_IMAGE,
33+
templateId: process.env.COMPUTESDK_TEMPLATE_ID,
34+
},
3135
}),
3236
});
3337

@@ -43,6 +47,7 @@ try {
4347
```
4448

4549
The `computesdk` provider handles sandbox creation, Sandbox Agent installation, agent setup, and server startup automatically. ComputeSDK routes to your configured provider behind the scenes.
50+
The `create` option now forwards the full ComputeSDK sandbox-create payload, including provider-specific fields such as `image` and `templateId` when the selected provider supports them.
4651

4752
Before calling `SandboxAgent.start()`, configure ComputeSDK with your provider:
4853

docs/deploy/e2b.mdx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ import { e2b } from "sandbox-agent/e2b";
2121
const envs: Record<string, string> = {};
2222
if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
2323
if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
24+
const template = process.env.E2B_TEMPLATE;
2425

2526
const sdk = await SandboxAgent.start({
2627
sandbox: e2b({
28+
template,
2729
create: { envs },
2830
}),
2931
});
@@ -41,7 +43,10 @@ try {
4143

4244
The `e2b` provider handles sandbox creation, Sandbox Agent installation, agent setup, and server startup automatically. Sandboxes pause by default instead of being deleted, and reconnecting with the same `sandboxId` resumes them automatically.
4345

46+
Pass `template` when you want to start from a custom E2B template alias or template ID. E2B base-image selection happens when you build the template, then `sandbox-agent/e2b` uses that template at sandbox creation time.
47+
4448
## Faster cold starts
4549

4650
For faster startup, create a custom E2B template with Sandbox Agent and target agents pre-installed.
47-
See [E2B Custom Templates](https://e2b.dev/docs/sandbox-template).
51+
Build System 2.0 also lets you choose the template's base image in code.
52+
See [E2B Custom Templates](https://e2b.dev/docs/sandbox-template) and [E2B Base Images](https://e2b.dev/docs/template/base-image).

docs/deploy/modal.mdx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ import { modal } from "sandbox-agent/modal";
2121
const secrets: Record<string, string> = {};
2222
if (process.env.ANTHROPIC_API_KEY) secrets.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
2323
if (process.env.OPENAI_API_KEY) secrets.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
24+
const baseImage = process.env.MODAL_BASE_IMAGE ?? "node:22-slim";
2425

2526
const sdk = await SandboxAgent.start({
2627
sandbox: modal({
28+
image: baseImage,
2729
create: { secrets },
2830
}),
2931
});
@@ -40,6 +42,7 @@ try {
4042
```
4143

4244
The `modal` provider handles app creation, image building, sandbox provisioning, agent installation, server startup, and tunnel networking automatically.
45+
Set `image` to change the base Docker image before Sandbox Agent and its agent binaries are layered on top. You can also pass a prebuilt Modal `Image` object.
4346

4447
## Faster cold starts
4548

examples/e2b/src/e2b.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ export async function setupE2BSandboxAgent(): Promise<{
1717
token?: string;
1818
cleanup: () => Promise<void>;
1919
}> {
20+
const template = process.env.E2B_TEMPLATE;
2021
const client = await SandboxAgent.start({
2122
sandbox: e2b({
23+
template,
2224
create: { envs: collectEnvVars() },
2325
}),
2426
});

examples/e2b/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import { detectAgent } from "@sandbox-agent/example-shared";
55
const envs: Record<string, string> = {};
66
if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
77
if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
8+
const template = process.env.E2B_TEMPLATE;
89

910
const client = await SandboxAgent.start({
1011
// ✨ NEW ✨
11-
sandbox: e2b({ create: { envs } }),
12+
sandbox: e2b({ template, create: { envs } }),
1213
});
1314

1415
const session = await client.createSession({

sdks/typescript/src/providers/computesdk.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,31 @@
1-
import { compute } from "computesdk";
1+
import { compute, type CreateSandboxOptions } from "computesdk";
22
import type { SandboxProvider } from "./types.ts";
33
import { DEFAULT_AGENTS, SANDBOX_AGENT_INSTALL_SCRIPT } from "./shared.ts";
44

55
const DEFAULT_AGENT_PORT = 3000;
66

7+
type ComputeCreateOverrides = Partial<CreateSandboxOptions>;
8+
79
export interface ComputeSdkProviderOptions {
8-
create?: {
9-
envs?: Record<string, string>;
10-
};
10+
create?: ComputeCreateOverrides | (() => ComputeCreateOverrides | Promise<ComputeCreateOverrides>);
1111
agentPort?: number;
1212
}
1313

14+
async function resolveCreateOptions(value: ComputeSdkProviderOptions["create"]): Promise<ComputeCreateOverrides> {
15+
if (!value) return {};
16+
return typeof value === "function" ? await value() : value;
17+
}
18+
1419
export function computesdk(options: ComputeSdkProviderOptions = {}): SandboxProvider {
1520
const agentPort = options.agentPort ?? DEFAULT_AGENT_PORT;
1621

1722
return {
1823
name: "computesdk",
1924
async create(): Promise<string> {
20-
const envs = options.create?.envs;
25+
const createOpts = await resolveCreateOptions(options.create);
2126
const sandbox = await compute.sandbox.create({
22-
envs: envs && Object.keys(envs).length > 0 ? envs : undefined,
27+
...createOpts,
28+
envs: createOpts.envs && Object.keys(createOpts.envs).length > 0 ? createOpts.envs : undefined,
2329
});
2430

2531
const run = async (cmd: string, runOptions?: { background?: boolean }) => {

sdks/typescript/src/providers/daytona.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ type DaytonaCreateOverrides = Partial<DaytonaCreateParams>;
1111

1212
export interface DaytonaProviderOptions {
1313
create?: DaytonaCreateOverrides | (() => DaytonaCreateOverrides | Promise<DaytonaCreateOverrides>);
14-
image?: string;
14+
image?: DaytonaCreateParams["image"];
1515
agentPort?: number;
1616
previewTtlSeconds?: number;
1717
deleteTimeoutSeconds?: number;

sdks/typescript/src/providers/e2b.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ const DEFAULT_TIMEOUT_MS = 3_600_000;
88

99
type E2BCreateOverrides = Omit<Partial<SandboxBetaCreateOpts>, "timeoutMs" | "autoPause">;
1010
type E2BConnectOverrides = Omit<Partial<SandboxConnectOpts>, "timeoutMs">;
11+
type E2BTemplateOverride = string | (() => string | Promise<string>);
1112

1213
export interface E2BProviderOptions {
1314
create?: E2BCreateOverrides | (() => E2BCreateOverrides | Promise<E2BCreateOverrides>);
1415
connect?: E2BConnectOverrides | ((sandboxId: string) => E2BConnectOverrides | Promise<E2BConnectOverrides>);
16+
template?: E2BTemplateOverride;
1517
agentPort?: number;
1618
timeoutMs?: number;
1719
autoPause?: boolean;
@@ -28,6 +30,11 @@ async function resolveOptions(value: E2BProviderOptions["create"] | E2BProviderO
2830
return value;
2931
}
3032

33+
async function resolveTemplate(value: E2BTemplateOverride | undefined): Promise<string | undefined> {
34+
if (!value) return undefined;
35+
return typeof value === "function" ? await value() : value;
36+
}
37+
3138
export function e2b(options: E2BProviderOptions = {}): SandboxProvider {
3239
const agentPort = options.agentPort ?? DEFAULT_AGENT_PORT;
3340
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
@@ -37,8 +44,16 @@ export function e2b(options: E2BProviderOptions = {}): SandboxProvider {
3744
name: "e2b",
3845
async create(): Promise<string> {
3946
const createOpts = await resolveOptions(options.create);
47+
const rawTemplate = typeof createOpts.template === "string" ? createOpts.template : undefined;
48+
const restCreateOpts = { ...createOpts };
49+
delete restCreateOpts.template;
50+
const template = (await resolveTemplate(options.template)) ?? rawTemplate;
4051
// eslint-disable-next-line @typescript-eslint/no-explicit-any
41-
const sandbox = await Sandbox.betaCreate({ allowInternetAccess: true, ...createOpts, timeoutMs, autoPause } as any);
52+
const sandbox = template
53+
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
54+
await Sandbox.betaCreate(template, { allowInternetAccess: true, ...restCreateOpts, timeoutMs, autoPause } as any)
55+
: // eslint-disable-next-line @typescript-eslint/no-explicit-any
56+
await Sandbox.betaCreate({ allowInternetAccess: true, ...restCreateOpts, timeoutMs, autoPause } as any);
4257

4358
await sandbox.commands.run(`curl -fsSL ${SANDBOX_AGENT_INSTALL_SCRIPT} | sh`).then((r) => {
4459
if (r.exitCode !== 0) throw new Error(`e2b install failed:\n${r.stderr}`);

sdks/typescript/src/providers/modal.ts

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,58 @@
1-
import { ModalClient } from "modal";
1+
import { ModalClient, type Image, type SandboxCreateParams } from "modal";
22
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";
44

55
const DEFAULT_AGENT_PORT = 3000;
66
const DEFAULT_APP_NAME = "sandbox-agent";
77
const DEFAULT_MEMORY_MIB = 2048;
88

9+
type ModalCreateOverrides = Omit<Partial<SandboxCreateParams>, "secrets" | "encryptedPorts"> & {
10+
secrets?: Record<string, string>;
11+
encryptedPorts?: number[];
12+
appName?: string;
13+
};
14+
915
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;
1518
agentPort?: number;
1619
}
1720

21+
async function resolveCreateOptions(value: ModalProviderOptions["create"]): Promise<ModalCreateOverrides> {
22+
if (!value) return {};
23+
return typeof value === "function" ? await value() : value;
24+
}
25+
1826
export function modal(options: ModalProviderOptions = {}): SandboxProvider {
1927
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;
2228
const client = new ModalClient();
2329

2430
return {
2531
name: "modal",
2632
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;
2736
const app = await client.apps.fromName(appName, { createIfMissing: true });
2837

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;
3941

40-
const envVars = options.create?.secrets ?? {};
42+
const envVars = createOpts.secrets ?? {};
4143
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;
4250

4351
const sb = await client.sandboxes.create(app, image, {
44-
encryptedPorts: [agentPort],
52+
...sandboxCreateOpts,
53+
encryptedPorts: [agentPort, ...extraPorts],
4554
secrets,
46-
memoryMiB,
55+
memoryMiB: sandboxCreateOpts.memoryMiB ?? DEFAULT_MEMORY_MIB,
4756
});
4857

4958
// Start the server as a long-running exec process. We intentionally

0 commit comments

Comments
 (0)