mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 07:04:48 +00:00
Merge pull request #269 from rivet-dev/e2b-base-image-support
feat(providers): add base image support and improve forward compatibility
This commit is contained in:
commit
32dd5914ed
10 changed files with 240 additions and 38 deletions
|
|
@ -27,7 +27,11 @@ if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY
|
||||||
|
|
||||||
const sdk = await SandboxAgent.start({
|
const sdk = await SandboxAgent.start({
|
||||||
sandbox: computesdk({
|
sandbox: computesdk({
|
||||||
create: { envs },
|
create: {
|
||||||
|
envs,
|
||||||
|
image: process.env.COMPUTESDK_IMAGE,
|
||||||
|
templateId: process.env.COMPUTESDK_TEMPLATE_ID,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -43,6 +47,7 @@ try {
|
||||||
```
|
```
|
||||||
|
|
||||||
The `computesdk` provider handles sandbox creation, Sandbox Agent installation, agent setup, and server startup automatically. ComputeSDK routes to your configured provider behind the scenes.
|
The `computesdk` provider handles sandbox creation, Sandbox Agent installation, agent setup, and server startup automatically. ComputeSDK routes to your configured provider behind the scenes.
|
||||||
|
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.
|
||||||
|
|
||||||
Before calling `SandboxAgent.start()`, configure ComputeSDK with your provider:
|
Before calling `SandboxAgent.start()`, configure ComputeSDK with your provider:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,11 @@ import { e2b } from "sandbox-agent/e2b";
|
||||||
const envs: Record<string, string> = {};
|
const envs: Record<string, string> = {};
|
||||||
if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
||||||
if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||||
|
const template = process.env.E2B_TEMPLATE;
|
||||||
|
|
||||||
const sdk = await SandboxAgent.start({
|
const sdk = await SandboxAgent.start({
|
||||||
sandbox: e2b({
|
sandbox: e2b({
|
||||||
|
template,
|
||||||
create: { envs },
|
create: { envs },
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
@ -41,7 +43,10 @@ try {
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
## Faster cold starts
|
## Faster cold starts
|
||||||
|
|
||||||
For faster startup, create a custom E2B template with Sandbox Agent and target agents pre-installed.
|
For faster startup, create a custom E2B template with Sandbox Agent and target agents pre-installed.
|
||||||
See [E2B Custom Templates](https://e2b.dev/docs/sandbox-template).
|
Build System 2.0 also lets you choose the template's base image in code.
|
||||||
|
See [E2B Custom Templates](https://e2b.dev/docs/sandbox-template) and [E2B Base Images](https://e2b.dev/docs/template/base-image).
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,11 @@ import { modal } from "sandbox-agent/modal";
|
||||||
const secrets: Record<string, string> = {};
|
const secrets: Record<string, string> = {};
|
||||||
if (process.env.ANTHROPIC_API_KEY) secrets.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
if (process.env.ANTHROPIC_API_KEY) secrets.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
||||||
if (process.env.OPENAI_API_KEY) secrets.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
if (process.env.OPENAI_API_KEY) secrets.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||||
|
const baseImage = process.env.MODAL_BASE_IMAGE ?? "node:22-slim";
|
||||||
|
|
||||||
const sdk = await SandboxAgent.start({
|
const sdk = await SandboxAgent.start({
|
||||||
sandbox: modal({
|
sandbox: modal({
|
||||||
|
image: baseImage,
|
||||||
create: { secrets },
|
create: { secrets },
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
@ -40,6 +42,7 @@ try {
|
||||||
```
|
```
|
||||||
|
|
||||||
The `modal` provider handles app creation, image building, sandbox provisioning, agent installation, server startup, and tunnel networking automatically.
|
The `modal` provider handles app creation, image building, sandbox provisioning, agent installation, server startup, and tunnel networking automatically.
|
||||||
|
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.
|
||||||
|
|
||||||
## Faster cold starts
|
## Faster cold starts
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,10 @@ export async function setupE2BSandboxAgent(): Promise<{
|
||||||
token?: string;
|
token?: string;
|
||||||
cleanup: () => Promise<void>;
|
cleanup: () => Promise<void>;
|
||||||
}> {
|
}> {
|
||||||
|
const template = process.env.E2B_TEMPLATE;
|
||||||
const client = await SandboxAgent.start({
|
const client = await SandboxAgent.start({
|
||||||
sandbox: e2b({
|
sandbox: e2b({
|
||||||
|
template,
|
||||||
create: { envs: collectEnvVars() },
|
create: { envs: collectEnvVars() },
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,11 @@ import { detectAgent } from "@sandbox-agent/example-shared";
|
||||||
const envs: Record<string, string> = {};
|
const envs: Record<string, string> = {};
|
||||||
if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
||||||
if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||||
|
const template = process.env.E2B_TEMPLATE;
|
||||||
|
|
||||||
const client = await SandboxAgent.start({
|
const client = await SandboxAgent.start({
|
||||||
// ✨ NEW ✨
|
// ✨ NEW ✨
|
||||||
sandbox: e2b({ create: { envs } }),
|
sandbox: e2b({ template, create: { envs } }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const session = await client.createSession({
|
const session = await client.createSession({
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,31 @@
|
||||||
import { compute } from "computesdk";
|
import { compute, type CreateSandboxOptions } from "computesdk";
|
||||||
import type { SandboxProvider } from "./types.ts";
|
import type { SandboxProvider } from "./types.ts";
|
||||||
import { DEFAULT_AGENTS, SANDBOX_AGENT_INSTALL_SCRIPT } from "./shared.ts";
|
import { DEFAULT_AGENTS, SANDBOX_AGENT_INSTALL_SCRIPT } from "./shared.ts";
|
||||||
|
|
||||||
const DEFAULT_AGENT_PORT = 3000;
|
const DEFAULT_AGENT_PORT = 3000;
|
||||||
|
|
||||||
|
type ComputeCreateOverrides = Partial<CreateSandboxOptions>;
|
||||||
|
|
||||||
export interface ComputeSdkProviderOptions {
|
export interface ComputeSdkProviderOptions {
|
||||||
create?: {
|
create?: ComputeCreateOverrides | (() => ComputeCreateOverrides | Promise<ComputeCreateOverrides>);
|
||||||
envs?: Record<string, string>;
|
|
||||||
};
|
|
||||||
agentPort?: number;
|
agentPort?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resolveCreateOptions(value: ComputeSdkProviderOptions["create"]): Promise<ComputeCreateOverrides> {
|
||||||
|
if (!value) return {};
|
||||||
|
return typeof value === "function" ? await value() : value;
|
||||||
|
}
|
||||||
|
|
||||||
export function computesdk(options: ComputeSdkProviderOptions = {}): SandboxProvider {
|
export function computesdk(options: ComputeSdkProviderOptions = {}): SandboxProvider {
|
||||||
const agentPort = options.agentPort ?? DEFAULT_AGENT_PORT;
|
const agentPort = options.agentPort ?? DEFAULT_AGENT_PORT;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: "computesdk",
|
name: "computesdk",
|
||||||
async create(): Promise<string> {
|
async create(): Promise<string> {
|
||||||
const envs = options.create?.envs;
|
const createOpts = await resolveCreateOptions(options.create);
|
||||||
const sandbox = await compute.sandbox.create({
|
const sandbox = await compute.sandbox.create({
|
||||||
envs: envs && Object.keys(envs).length > 0 ? envs : undefined,
|
...createOpts,
|
||||||
|
envs: createOpts.envs && Object.keys(createOpts.envs).length > 0 ? createOpts.envs : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const run = async (cmd: string, runOptions?: { background?: boolean }) => {
|
const run = async (cmd: string, runOptions?: { background?: boolean }) => {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ type DaytonaCreateOverrides = Partial<DaytonaCreateParams>;
|
||||||
|
|
||||||
export interface DaytonaProviderOptions {
|
export interface DaytonaProviderOptions {
|
||||||
create?: DaytonaCreateOverrides | (() => DaytonaCreateOverrides | Promise<DaytonaCreateOverrides>);
|
create?: DaytonaCreateOverrides | (() => DaytonaCreateOverrides | Promise<DaytonaCreateOverrides>);
|
||||||
image?: string;
|
image?: DaytonaCreateParams["image"];
|
||||||
agentPort?: number;
|
agentPort?: number;
|
||||||
previewTtlSeconds?: number;
|
previewTtlSeconds?: number;
|
||||||
deleteTimeoutSeconds?: number;
|
deleteTimeoutSeconds?: number;
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,12 @@ const DEFAULT_TIMEOUT_MS = 3_600_000;
|
||||||
|
|
||||||
type E2BCreateOverrides = Omit<Partial<SandboxBetaCreateOpts>, "timeoutMs" | "autoPause">;
|
type E2BCreateOverrides = Omit<Partial<SandboxBetaCreateOpts>, "timeoutMs" | "autoPause">;
|
||||||
type E2BConnectOverrides = Omit<Partial<SandboxConnectOpts>, "timeoutMs">;
|
type E2BConnectOverrides = Omit<Partial<SandboxConnectOpts>, "timeoutMs">;
|
||||||
|
type E2BTemplateOverride = string | (() => string | Promise<string>);
|
||||||
|
|
||||||
export interface E2BProviderOptions {
|
export interface E2BProviderOptions {
|
||||||
create?: E2BCreateOverrides | (() => E2BCreateOverrides | Promise<E2BCreateOverrides>);
|
create?: E2BCreateOverrides | (() => E2BCreateOverrides | Promise<E2BCreateOverrides>);
|
||||||
connect?: E2BConnectOverrides | ((sandboxId: string) => E2BConnectOverrides | Promise<E2BConnectOverrides>);
|
connect?: E2BConnectOverrides | ((sandboxId: string) => E2BConnectOverrides | Promise<E2BConnectOverrides>);
|
||||||
|
template?: E2BTemplateOverride;
|
||||||
agentPort?: number;
|
agentPort?: number;
|
||||||
timeoutMs?: number;
|
timeoutMs?: number;
|
||||||
autoPause?: boolean;
|
autoPause?: boolean;
|
||||||
|
|
@ -28,6 +30,11 @@ async function resolveOptions(value: E2BProviderOptions["create"] | E2BProviderO
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resolveTemplate(value: E2BTemplateOverride | undefined): Promise<string | undefined> {
|
||||||
|
if (!value) return undefined;
|
||||||
|
return typeof value === "function" ? await value() : value;
|
||||||
|
}
|
||||||
|
|
||||||
export function e2b(options: E2BProviderOptions = {}): SandboxProvider {
|
export function e2b(options: E2BProviderOptions = {}): SandboxProvider {
|
||||||
const agentPort = options.agentPort ?? DEFAULT_AGENT_PORT;
|
const agentPort = options.agentPort ?? DEFAULT_AGENT_PORT;
|
||||||
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
||||||
|
|
@ -37,8 +44,16 @@ export function e2b(options: E2BProviderOptions = {}): SandboxProvider {
|
||||||
name: "e2b",
|
name: "e2b",
|
||||||
async create(): Promise<string> {
|
async create(): Promise<string> {
|
||||||
const createOpts = await resolveOptions(options.create);
|
const createOpts = await resolveOptions(options.create);
|
||||||
|
const rawTemplate = typeof createOpts.template === "string" ? createOpts.template : undefined;
|
||||||
|
const restCreateOpts = { ...createOpts };
|
||||||
|
delete restCreateOpts.template;
|
||||||
|
const template = (await resolveTemplate(options.template)) ?? rawTemplate;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const sandbox = await Sandbox.betaCreate({ allowInternetAccess: true, ...createOpts, timeoutMs, autoPause } as any);
|
const sandbox = template
|
||||||
|
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
await Sandbox.betaCreate(template, { allowInternetAccess: true, ...restCreateOpts, timeoutMs, autoPause } as any)
|
||||||
|
: // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
await Sandbox.betaCreate({ allowInternetAccess: true, ...restCreateOpts, timeoutMs, autoPause } as any);
|
||||||
|
|
||||||
await sandbox.commands.run(`curl -fsSL ${SANDBOX_AGENT_INSTALL_SCRIPT} | sh`).then((r) => {
|
await sandbox.commands.run(`curl -fsSL ${SANDBOX_AGENT_INSTALL_SCRIPT} | sh`).then((r) => {
|
||||||
if (r.exitCode !== 0) throw new Error(`e2b install failed:\n${r.stderr}`);
|
if (r.exitCode !== 0) throw new Error(`e2b install failed:\n${r.stderr}`);
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,58 @@
|
||||||
import { ModalClient } from "modal";
|
import { ModalClient, type Image, type SandboxCreateParams } from "modal";
|
||||||
import type { SandboxProvider } from "./types.ts";
|
import type { SandboxProvider } from "./types.ts";
|
||||||
import { DEFAULT_AGENTS, SANDBOX_AGENT_INSTALL_SCRIPT } from "./shared.ts";
|
import { DEFAULT_SANDBOX_AGENT_IMAGE } from "./shared.ts";
|
||||||
|
|
||||||
const DEFAULT_AGENT_PORT = 3000;
|
const DEFAULT_AGENT_PORT = 3000;
|
||||||
const DEFAULT_APP_NAME = "sandbox-agent";
|
const DEFAULT_APP_NAME = "sandbox-agent";
|
||||||
const DEFAULT_MEMORY_MIB = 2048;
|
const DEFAULT_MEMORY_MIB = 2048;
|
||||||
|
|
||||||
export interface ModalProviderOptions {
|
type ModalCreateOverrides = Omit<Partial<SandboxCreateParams>, "secrets" | "encryptedPorts"> & {
|
||||||
create?: {
|
|
||||||
secrets?: Record<string, string>;
|
secrets?: Record<string, string>;
|
||||||
|
encryptedPorts?: number[];
|
||||||
appName?: string;
|
appName?: string;
|
||||||
memoryMiB?: number;
|
};
|
||||||
};
|
|
||||||
|
export interface ModalProviderOptions {
|
||||||
|
create?: ModalCreateOverrides | (() => ModalCreateOverrides | Promise<ModalCreateOverrides>);
|
||||||
|
image?: string | Image;
|
||||||
agentPort?: number;
|
agentPort?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resolveCreateOptions(value: ModalProviderOptions["create"]): Promise<ModalCreateOverrides> {
|
||||||
|
if (!value) return {};
|
||||||
|
return typeof value === "function" ? await value() : value;
|
||||||
|
}
|
||||||
|
|
||||||
export function modal(options: ModalProviderOptions = {}): SandboxProvider {
|
export function modal(options: ModalProviderOptions = {}): SandboxProvider {
|
||||||
const agentPort = options.agentPort ?? DEFAULT_AGENT_PORT;
|
const agentPort = options.agentPort ?? DEFAULT_AGENT_PORT;
|
||||||
const appName = options.create?.appName ?? DEFAULT_APP_NAME;
|
|
||||||
const memoryMiB = options.create?.memoryMiB ?? DEFAULT_MEMORY_MIB;
|
|
||||||
const client = new ModalClient();
|
const client = new ModalClient();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: "modal",
|
name: "modal",
|
||||||
async create(): Promise<string> {
|
async create(): Promise<string> {
|
||||||
|
const createOpts = await resolveCreateOptions(options.create);
|
||||||
|
const appName = createOpts.appName ?? DEFAULT_APP_NAME;
|
||||||
|
const baseImage = options.image ?? DEFAULT_SANDBOX_AGENT_IMAGE;
|
||||||
const app = await client.apps.fromName(appName, { createIfMissing: true });
|
const app = await client.apps.fromName(appName, { createIfMissing: true });
|
||||||
|
|
||||||
// Pre-install sandbox-agent and agents in the image so they are cached
|
// The default `-full` base image already includes sandbox-agent and all
|
||||||
// across sandbox creates and don't need to be installed at runtime.
|
// agents pre-installed, so no additional dockerfile commands are needed.
|
||||||
const installAgentCmds = DEFAULT_AGENTS.map((agent) => `RUN sandbox-agent install-agent ${agent}`);
|
const image = typeof baseImage === "string" ? client.images.fromRegistry(baseImage) : baseImage;
|
||||||
const image = client.images
|
|
||||||
.fromRegistry("node:22-slim")
|
|
||||||
.dockerfileCommands([
|
|
||||||
"RUN apt-get update && apt-get install -y curl ca-certificates && rm -rf /var/lib/apt/lists/*",
|
|
||||||
`RUN curl -fsSL ${SANDBOX_AGENT_INSTALL_SCRIPT} | sh`,
|
|
||||||
...installAgentCmds,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const envVars = options.create?.secrets ?? {};
|
const envVars = createOpts.secrets ?? {};
|
||||||
const secrets = Object.keys(envVars).length > 0 ? [await client.secrets.fromObject(envVars)] : [];
|
const secrets = Object.keys(envVars).length > 0 ? [await client.secrets.fromObject(envVars)] : [];
|
||||||
|
const sandboxCreateOpts = { ...createOpts };
|
||||||
|
delete sandboxCreateOpts.appName;
|
||||||
|
delete sandboxCreateOpts.secrets;
|
||||||
|
|
||||||
|
const extraPorts = createOpts.encryptedPorts ?? [];
|
||||||
|
delete sandboxCreateOpts.encryptedPorts;
|
||||||
|
|
||||||
const sb = await client.sandboxes.create(app, image, {
|
const sb = await client.sandboxes.create(app, image, {
|
||||||
encryptedPorts: [agentPort],
|
...sandboxCreateOpts,
|
||||||
|
encryptedPorts: [agentPort, ...extraPorts],
|
||||||
secrets,
|
secrets,
|
||||||
memoryMiB,
|
memoryMiB: sandboxCreateOpts.memoryMiB ?? DEFAULT_MEMORY_MIB,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start the server as a long-running exec process. We intentionally
|
// Start the server as a long-running exec process. We intentionally
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,19 @@ const e2bMocks = vi.hoisted(() => {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const modalMocks = vi.hoisted(() => ({
|
||||||
|
appsFromName: vi.fn(),
|
||||||
|
imageFromRegistry: vi.fn(),
|
||||||
|
secretFromObject: vi.fn(),
|
||||||
|
sandboxCreate: vi.fn(),
|
||||||
|
sandboxFromId: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const computeSdkMocks = vi.hoisted(() => ({
|
||||||
|
create: vi.fn(),
|
||||||
|
getById: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("@e2b/code-interpreter", () => ({
|
vi.mock("@e2b/code-interpreter", () => ({
|
||||||
NotFoundError: e2bMocks.MockNotFoundError,
|
NotFoundError: e2bMocks.MockNotFoundError,
|
||||||
Sandbox: {
|
Sandbox: {
|
||||||
|
|
@ -24,7 +37,30 @@ vi.mock("@e2b/code-interpreter", () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("modal", () => ({
|
||||||
|
ModalClient: class MockModalClient {
|
||||||
|
apps = { fromName: modalMocks.appsFromName };
|
||||||
|
images = { fromRegistry: modalMocks.imageFromRegistry };
|
||||||
|
secrets = { fromObject: modalMocks.secretFromObject };
|
||||||
|
sandboxes = {
|
||||||
|
create: modalMocks.sandboxCreate,
|
||||||
|
fromId: modalMocks.sandboxFromId,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("computesdk", () => ({
|
||||||
|
compute: {
|
||||||
|
sandbox: {
|
||||||
|
create: computeSdkMocks.create,
|
||||||
|
getById: computeSdkMocks.getById,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
import { e2b } from "../src/providers/e2b.ts";
|
import { e2b } from "../src/providers/e2b.ts";
|
||||||
|
import { modal } from "../src/providers/modal.ts";
|
||||||
|
import { computesdk } from "../src/providers/computesdk.ts";
|
||||||
|
|
||||||
function createFetch(): typeof fetch {
|
function createFetch(): typeof fetch {
|
||||||
return async () => new Response(null, { status: 200 });
|
return async () => new Response(null, { status: 200 });
|
||||||
|
|
@ -56,6 +92,26 @@ function createMockSandbox() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createMockModalImage() {
|
||||||
|
return {
|
||||||
|
dockerfileCommands: vi.fn(function dockerfileCommands() {
|
||||||
|
return this;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
e2bMocks.betaCreate.mockReset();
|
||||||
|
e2bMocks.connect.mockReset();
|
||||||
|
modalMocks.appsFromName.mockReset();
|
||||||
|
modalMocks.imageFromRegistry.mockReset();
|
||||||
|
modalMocks.secretFromObject.mockReset();
|
||||||
|
modalMocks.sandboxCreate.mockReset();
|
||||||
|
modalMocks.sandboxFromId.mockReset();
|
||||||
|
computeSdkMocks.create.mockReset();
|
||||||
|
computeSdkMocks.getById.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
describe("SandboxAgent provider lifecycle", () => {
|
describe("SandboxAgent provider lifecycle", () => {
|
||||||
it("reconnects an existing sandbox before ensureServer", async () => {
|
it("reconnects an existing sandbox before ensureServer", async () => {
|
||||||
const order: string[] = [];
|
const order: string[] = [];
|
||||||
|
|
@ -124,11 +180,6 @@ describe("SandboxAgent provider lifecycle", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("e2b provider", () => {
|
describe("e2b provider", () => {
|
||||||
beforeEach(() => {
|
|
||||||
e2bMocks.betaCreate.mockReset();
|
|
||||||
e2bMocks.connect.mockReset();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("creates sandboxes with betaCreate, autoPause, and the default timeout", async () => {
|
it("creates sandboxes with betaCreate, autoPause, and the default timeout", async () => {
|
||||||
const sandbox = createMockSandbox();
|
const sandbox = createMockSandbox();
|
||||||
e2bMocks.betaCreate.mockResolvedValue(sandbox);
|
e2bMocks.betaCreate.mockResolvedValue(sandbox);
|
||||||
|
|
@ -190,4 +241,109 @@ describe("e2b provider", () => {
|
||||||
|
|
||||||
await expect(provider.reconnect?.("missing-sandbox")).rejects.toBeInstanceOf(SandboxDestroyedError);
|
await expect(provider.reconnect?.("missing-sandbox")).rejects.toBeInstanceOf(SandboxDestroyedError);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("passes a configured template to betaCreate", async () => {
|
||||||
|
const sandbox = createMockSandbox();
|
||||||
|
e2bMocks.betaCreate.mockResolvedValue(sandbox);
|
||||||
|
|
||||||
|
const provider = e2b({
|
||||||
|
template: "my-template",
|
||||||
|
create: { envs: { ANTHROPIC_API_KEY: "test" } },
|
||||||
|
});
|
||||||
|
|
||||||
|
await provider.create();
|
||||||
|
|
||||||
|
expect(e2bMocks.betaCreate).toHaveBeenCalledWith(
|
||||||
|
"my-template",
|
||||||
|
expect.objectContaining({
|
||||||
|
allowInternetAccess: true,
|
||||||
|
envs: { ANTHROPIC_API_KEY: "test" },
|
||||||
|
timeoutMs: 3_600_000,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts legacy create.template values from plain JavaScript", async () => {
|
||||||
|
const sandbox = createMockSandbox();
|
||||||
|
e2bMocks.betaCreate.mockResolvedValue(sandbox);
|
||||||
|
|
||||||
|
const provider = e2b({
|
||||||
|
create: { template: "legacy-template" } as never,
|
||||||
|
});
|
||||||
|
|
||||||
|
await provider.create();
|
||||||
|
|
||||||
|
expect(e2bMocks.betaCreate).toHaveBeenCalledWith(
|
||||||
|
"legacy-template",
|
||||||
|
expect.objectContaining({
|
||||||
|
allowInternetAccess: true,
|
||||||
|
timeoutMs: 3_600_000,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("modal provider", () => {
|
||||||
|
it("uses the configured base image when building the sandbox image", async () => {
|
||||||
|
const app = { appId: "app-123" };
|
||||||
|
const image = createMockModalImage();
|
||||||
|
const sandbox = {
|
||||||
|
sandboxId: "sbx-modal",
|
||||||
|
exec: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
modalMocks.appsFromName.mockResolvedValue(app);
|
||||||
|
modalMocks.imageFromRegistry.mockReturnValue(image);
|
||||||
|
modalMocks.sandboxCreate.mockResolvedValue(sandbox);
|
||||||
|
|
||||||
|
const provider = modal({
|
||||||
|
image: "python:3.12-slim",
|
||||||
|
create: {
|
||||||
|
appName: "custom-app",
|
||||||
|
secrets: { OPENAI_API_KEY: "test" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(provider.create()).resolves.toBe("sbx-modal");
|
||||||
|
|
||||||
|
expect(modalMocks.appsFromName).toHaveBeenCalledWith("custom-app", { createIfMissing: true });
|
||||||
|
expect(modalMocks.imageFromRegistry).toHaveBeenCalledWith("python:3.12-slim");
|
||||||
|
expect(image.dockerfileCommands).toHaveBeenCalled();
|
||||||
|
expect(modalMocks.sandboxCreate).toHaveBeenCalledWith(
|
||||||
|
app,
|
||||||
|
image,
|
||||||
|
expect.objectContaining({
|
||||||
|
encryptedPorts: [3000],
|
||||||
|
memoryMiB: 2048,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("computesdk provider", () => {
|
||||||
|
it("passes image and template options through to compute.sandbox.create", async () => {
|
||||||
|
const sandbox = {
|
||||||
|
sandboxId: "sbx-compute",
|
||||||
|
runCommand: vi.fn(async () => ({ exitCode: 0, stderr: "" })),
|
||||||
|
};
|
||||||
|
computeSdkMocks.create.mockResolvedValue(sandbox);
|
||||||
|
|
||||||
|
const provider = computesdk({
|
||||||
|
create: {
|
||||||
|
envs: { ANTHROPIC_API_KEY: "test" },
|
||||||
|
image: "ghcr.io/example/sandbox-agent:latest",
|
||||||
|
templateId: "tmpl-123",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(provider.create()).resolves.toBe("sbx-compute");
|
||||||
|
|
||||||
|
expect(computeSdkMocks.create).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
envs: { ANTHROPIC_API_KEY: "test" },
|
||||||
|
image: "ghcr.io/example/sandbox-agent:latest",
|
||||||
|
templateId: "tmpl-123",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue