Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 44 additions & 3 deletions docs/deploy/daytona.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,44 @@ The `daytona` provider uses the `rivetdev/sandbox-agent:0.4.2-full` image by def

```typescript
import { Daytona, Image } from "@daytonaio/sdk";
import { SandboxAgent } from "sandbox-agent";
import { daytona } from "sandbox-agent/daytona";

class DaytonaSnapshotNotFoundError extends Error {
constructor(snapshotName: string, options?: ErrorOptions) {
super(`daytona snapshot not found: ${snapshotName}`, options);
this.name = "DaytonaSnapshotNotFoundError";
}
}

const daytona = new Daytona();
async function ensureDaytonaSnapshotExists(client: Daytona, snapshotName: string): Promise<void> {
try {
await client.snapshot.get(snapshotName);
} catch (error) {
if (typeof error === "object" && error !== null && "statusCode" in error && error.statusCode === 404) {
throw new DaytonaSnapshotNotFoundError(snapshotName, { cause: error });
}
throw error;
}
}

const client = new Daytona();
const SNAPSHOT = "sandbox-agent-ready";
const envVars: Record<string, string> = {};

if (process.env.ANTHROPIC_API_KEY) envVars.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
if (process.env.OPENAI_API_KEY) envVars.OPENAI_API_KEY = process.env.OPENAI_API_KEY;

const hasSnapshot = await daytona.snapshot.get(SNAPSHOT).then(() => true, () => false);
const hasSnapshot = await ensureDaytonaSnapshotExists(client, SNAPSHOT).then(
() => true,
(error) => {
if (error instanceof DaytonaSnapshotNotFoundError) return false;
throw error;
},
);

if (!hasSnapshot) {
await daytona.snapshot.create({
await client.snapshot.create({
name: SNAPSHOT,
image: Image.base("ubuntu:22.04").runCommands(
"apt-get update && apt-get install -y curl ca-certificates",
Expand All @@ -67,4 +97,15 @@ if (!hasSnapshot) {
),
});
}

const sdk = await SandboxAgent.start({
sandbox: daytona({
create: {
snapshot: SNAPSHOT,
envVars,
},
}),
});
```

When `create.snapshot` is provided, the Daytona provider starts from that existing snapshot instead of forcing the default image.
44 changes: 43 additions & 1 deletion examples/daytona/src/daytona.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
import { Daytona, Image } from "@daytonaio/sdk";
import { SandboxAgent } from "sandbox-agent";
import { daytona } from "sandbox-agent/daytona";

const SNAPSHOT = "sandbox-agent-ready";

class DaytonaSnapshotNotFoundError extends Error {
constructor(snapshotName: string, options?: ErrorOptions) {
super(`daytona snapshot not found: ${snapshotName}`, options);
this.name = "DaytonaSnapshotNotFoundError";
}
}

async function ensureDaytonaSnapshotExists(client: Daytona, snapshotName: string): Promise<void> {
try {
await client.snapshot.get(snapshotName);
} catch (error) {
if (typeof error === "object" && error !== null && "statusCode" in error && error.statusCode === 404) {
throw new DaytonaSnapshotNotFoundError(snapshotName, { cause: error });
}
throw error;
}
}

function collectEnvVars(): Record<string, string> {
const envVars: Record<string, string> = {};
if (process.env.ANTHROPIC_API_KEY) envVars.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
Expand All @@ -18,9 +39,30 @@ export async function setupDaytonaSandboxAgent(): Promise<{
extraHeaders?: Record<string, string>;
cleanup: () => Promise<void>;
}> {
const daytonaClient = new Daytona();
const hasSnapshot = await ensureDaytonaSnapshotExists(daytonaClient, SNAPSHOT).then(
() => true,
(error) => {
if (error instanceof DaytonaSnapshotNotFoundError) return false;
throw error;
},
);

if (!hasSnapshot) {
await daytonaClient.snapshot.create({
name: SNAPSHOT,
image: Image.base("ubuntu:22.04").runCommands(
"apt-get update && apt-get install -y curl ca-certificates",
"curl -fsSL https://releases.rivet.dev/sandbox-agent/0.4.x/install.sh | sh",
"sandbox-agent install-agent claude",
"sandbox-agent install-agent codex",
),
});
}

const client = await SandboxAgent.start({
sandbox: daytona({
create: { envVars: collectEnvVars() },
create: { snapshot: SNAPSHOT, envVars: collectEnvVars() },
}),
});

Expand Down
43 changes: 42 additions & 1 deletion examples/daytona/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,55 @@
import { Daytona, Image } from "@daytonaio/sdk";
import { SandboxAgent } from "sandbox-agent";
import { daytona } from "sandbox-agent/daytona";
import { detectAgent } from "@sandbox-agent/example-shared";

const SNAPSHOT = "sandbox-agent-ready";
const envVars: Record<string, string> = {};
if (process.env.ANTHROPIC_API_KEY) envVars.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
if (process.env.OPENAI_API_KEY) envVars.OPENAI_API_KEY = process.env.OPENAI_API_KEY;

class DaytonaSnapshotNotFoundError extends Error {
constructor(snapshotName: string, options?: ErrorOptions) {
super(`daytona snapshot not found: ${snapshotName}`, options);
this.name = "DaytonaSnapshotNotFoundError";
}
}

async function ensureDaytonaSnapshotExists(client: Daytona, snapshotName: string): Promise<void> {
try {
await client.snapshot.get(snapshotName);
} catch (error) {
if (typeof error === "object" && error !== null && "statusCode" in error && error.statusCode === 404) {
throw new DaytonaSnapshotNotFoundError(snapshotName, { cause: error });
}
throw error;
}
}

const daytonaClient = new Daytona();
const hasSnapshot = await ensureDaytonaSnapshotExists(daytonaClient, SNAPSHOT).then(
() => true,
(error) => {
if (error instanceof DaytonaSnapshotNotFoundError) return false;
throw error;
},
);

if (!hasSnapshot) {
await daytonaClient.snapshot.create({
name: SNAPSHOT,
image: Image.base("ubuntu:22.04").runCommands(
"apt-get update && apt-get install -y curl ca-certificates",
"curl -fsSL https://releases.rivet.dev/sandbox-agent/0.4.x/install.sh | sh",
"sandbox-agent install-agent claude",
"sandbox-agent install-agent codex",
),
});
}

const client = await SandboxAgent.start({
sandbox: daytona({
create: { envVars },
create: { snapshot: SNAPSHOT, envVars },
}),
});

Expand Down
39 changes: 29 additions & 10 deletions sdks/typescript/src/providers/daytona.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import { Daytona } from "@daytonaio/sdk";
import { Daytona, type CreateSandboxFromImageParams, type CreateSandboxFromSnapshotParams } from "@daytonaio/sdk";
import type { SandboxProvider } from "./types.ts";
import { DEFAULT_SANDBOX_AGENT_IMAGE, buildServerStartCommand } from "./shared.ts";

const DEFAULT_AGENT_PORT = 3000;
const DEFAULT_PREVIEW_TTL_SECONDS = 4 * 60 * 60;
const DEFAULT_CWD = "/home/sandbox";

type DaytonaCreateParams = NonNullable<Parameters<Daytona["create"]>[0]>;

type DaytonaCreateOverrides = Partial<DaytonaCreateParams>;
type DaytonaCreateOverrides = Partial<CreateSandboxFromImageParams | CreateSandboxFromSnapshotParams>;

export interface DaytonaProviderOptions {
create?: DaytonaCreateOverrides | (() => DaytonaCreateOverrides | Promise<DaytonaCreateOverrides>);
image?: DaytonaCreateParams["image"];
image?: CreateSandboxFromImageParams["image"];
agentPort?: number;
cwd?: string;
previewTtlSeconds?: number;
Expand All @@ -25,6 +23,14 @@ async function resolveCreateOptions(value: DaytonaProviderOptions["create"]): Pr
return value;
}

function getSnapshotName(createOpts: DaytonaCreateOverrides | undefined): string | undefined {
if (!createOpts || !Object.prototype.hasOwnProperty.call(createOpts, "snapshot")) {
return undefined;
}
const snapshot = (createOpts as CreateSandboxFromSnapshotParams).snapshot;
return typeof snapshot === "string" && snapshot.length > 0 ? snapshot : undefined;
}

export function daytona(options: DaytonaProviderOptions = {}): SandboxProvider {
const agentPort = options.agentPort ?? DEFAULT_AGENT_PORT;
const image = options.image ?? DEFAULT_SANDBOX_AGENT_IMAGE;
Expand All @@ -37,11 +43,24 @@ export function daytona(options: DaytonaProviderOptions = {}): SandboxProvider {
defaultCwd: cwd,
async create(): Promise<string> {
const createOpts = await resolveCreateOptions(options.create);
const sandbox = await client.create({
image,
autoStopInterval: 0,
...createOpts,
} as DaytonaCreateParams);
const snapshot = getSnapshotName(createOpts);
const hasSnapshot = snapshot !== undefined;
const createImage = createOpts && "image" in createOpts ? createOpts.image : undefined;

if (hasSnapshot && (options.image !== undefined || createImage !== undefined)) {
throw new Error("daytona provider does not support combining snapshot with image; pass only create.snapshot or image");
}

const sandbox = hasSnapshot
? await client.create({
autoStopInterval: 0,
...createOpts,
} as CreateSandboxFromSnapshotParams)
: await client.create({
image,
autoStopInterval: 0,
...createOpts,
} as CreateSandboxFromImageParams);
await sandbox.process.executeCommand(buildServerStartCommand(agentPort));
return sandbox.id;
},
Expand Down
81 changes: 81 additions & 0 deletions sdks/typescript/tests/provider-lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ const computeSdkMocks = vi.hoisted(() => ({
getById: vi.fn(),
}));

const daytonaMocks = vi.hoisted(() => ({
create: vi.fn(),
get: vi.fn(),
}));

const spritesMocks = vi.hoisted(() => ({
createSprite: vi.fn(),
getSprite: vi.fn(),
Expand Down Expand Up @@ -64,6 +69,13 @@ vi.mock("computesdk", () => ({
},
}));

vi.mock("@daytonaio/sdk", () => ({
Daytona: class MockDaytona {
create = daytonaMocks.create;
get = daytonaMocks.get;
},
}));

vi.mock("@fly/sprites", () => ({
SpritesClient: class MockSpritesClient {
readonly token: string;
Expand All @@ -85,6 +97,7 @@ import { modal } from "../src/providers/modal.ts";
import { computesdk } from "../src/providers/computesdk.ts";
import { agentcomputer } from "../src/providers/agentcomputer.ts";
import { sprites } from "../src/providers/sprites.ts";
import { daytona } from "../src/providers/daytona.ts";

function createFetch(): typeof fetch {
return async () => new Response(null, { status: 200 });
Expand Down Expand Up @@ -135,6 +148,17 @@ function createMockModalImage() {
};
}

function createMockDaytonaSandbox() {
return {
id: "daytona-123",
process: {
executeCommand: vi.fn(async () => ({ result: "" })),
},
delete: vi.fn(async () => undefined),
getSignedPreviewUrl: vi.fn(async () => ({ url: "https://daytona.example" })),
};
}

beforeEach(() => {
e2bMocks.betaCreate.mockReset();
e2bMocks.connect.mockReset();
Expand All @@ -145,6 +169,8 @@ beforeEach(() => {
modalMocks.sandboxFromId.mockReset();
computeSdkMocks.create.mockReset();
computeSdkMocks.getById.mockReset();
daytonaMocks.create.mockReset();
daytonaMocks.get.mockReset();
spritesMocks.createSprite.mockReset();
spritesMocks.getSprite.mockReset();
spritesMocks.deleteSprite.mockReset();
Expand Down Expand Up @@ -342,6 +368,61 @@ describe("e2b provider", () => {
});
});

describe("daytona provider", () => {
it("creates sandboxes from snapshots without injecting the default image", async () => {
const sandbox = createMockDaytonaSandbox();
daytonaMocks.create.mockResolvedValue(sandbox);

const provider = daytona({
create: {
snapshot: "sandbox-agent-ready",
envVars: { ANTHROPIC_API_KEY: "test" },
},
});

await expect(provider.create()).resolves.toBe("daytona-123");

expect(daytonaMocks.create).toHaveBeenCalledWith({
autoStopInterval: 0,
snapshot: "sandbox-agent-ready",
envVars: { ANTHROPIC_API_KEY: "test" },
});
});

it("creates sandboxes from the configured image when no snapshot is provided", async () => {
const sandbox = createMockDaytonaSandbox();
daytonaMocks.create.mockResolvedValue(sandbox);

const provider = daytona({
image: "ghcr.io/example/custom-image:latest",
create: {
envVars: { OPENAI_API_KEY: "test" },
},
});

await provider.create();

expect(daytonaMocks.create).toHaveBeenCalledWith({
image: "ghcr.io/example/custom-image:latest",
autoStopInterval: 0,
envVars: { OPENAI_API_KEY: "test" },
});
});

it("rejects combining snapshot and image inputs", async () => {
const provider = daytona({
image: "ghcr.io/example/custom-image:latest",
create: {
snapshot: "sandbox-agent-ready",
},
});

await expect(provider.create()).rejects.toThrow("daytona provider does not support combining snapshot with image; pass only create.snapshot or image");

expect(daytonaMocks.create).not.toHaveBeenCalled();
});
});

describe("modal provider", () => {
it("uses the configured base image when building the sandbox image", async () => {
const app = { appId: "app-123" };
Expand Down