Skip to content
Merged
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
4 changes: 2 additions & 2 deletions examples/snapshots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ async function main(): Promise<void> {
try {
await sandbox.filesystem.writeFile("/workspace/checkpoint.txt", "before snapshot\n");

const snapshot = await client.snapshots.create(sandbox, { name: "example-checkpoint" });
const snapshot = await sandbox.createSnapshot({ name: "example-checkpoint" });
console.log("snapshot:", snapshot.id);

const restored = await client.snapshots.resume({
const restored = await client.snapshots.restore({
snapshotName: snapshot.name ?? "example-checkpoint",
});
try {
Expand Down
17 changes: 17 additions & 0 deletions src/client/sandbox.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {
CreateSnapshotParams,
ObjectStorageMount,
ObjectStorageMountSummary,
ObjectStorageMountUpdate,
Expand Down Expand Up @@ -208,6 +209,22 @@ export class Sandbox implements SandboxData {
return this;
}

/**
* Creates a snapshot from this sandbox.
*
* @param params Optional snapshot naming and lifecycle parameters.
* @param options Optional request settings.
* @returns The created snapshot resource.
*
* @throws {Leap0Error} If creating the snapshot fails.
*/
async createSnapshot(
params: CreateSnapshotParams = {},
options?: { timeout?: number },
) {
return this.client.sandboxes.createSnapshot(this.id, params, options);
}

/**
* Deletes the sandbox.
*
Expand Down
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ export type {
CodeExecutionResult,
CreatePtySessionParams,
CreateSandboxParams,
CreateSnapshotParams,
ListSandboxesParams,
ListSandboxesResponse,
CreateSnapshotParams,
ListSnapshotsParams,
ListSnapshotsResponse,
CreateTemplateParams,
Expand Down Expand Up @@ -85,7 +85,7 @@ export type {
ProcessResult,
PresignedUrl,
PtySession,
ResumeSnapshotParams,
RestoreSnapshotParams,
SandboxListItem,
SearchMatch,
SshAccess,
Expand Down
7 changes: 4 additions & 3 deletions src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type { SandboxRef, SnapshotRef, TemplateRef } from "@/models/refs.js";
export {
createPresignedUrlParamsSchema,
createSandboxParamsSchema,
createSnapshotParamsSchema,
listSandboxesParamsSchema,
listSandboxesResponseSchema,
NetworkPolicyMode,
Expand All @@ -17,6 +18,7 @@ export {
export type {
CreatePresignedUrlParams,
CreateSandboxParams,
CreateSnapshotParams,
ListSandboxesParams,
ListSandboxesResponse,
NetworkPolicy,
Expand All @@ -29,15 +31,14 @@ export type {
} from "@/models/sandbox.js";

export {
createSnapshotParamsSchema,
listSnapshotsParamsSchema,
listSnapshotsResponseSchema,
restoreSnapshotParamsSchema,
} from "@/models/snapshot.js";
export type {
CreateSnapshotParams,
ListSnapshotsParams,
ListSnapshotsResponse,
ResumeSnapshotParams,
RestoreSnapshotParams,
SnapshotData,
} from "@/models/snapshot.js";

Expand Down
7 changes: 7 additions & 0 deletions src/models/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,13 @@ export const sandboxDataSchema = z
/** Sandbox resource returned by the control plane API. */
export type SandboxData = z.infer<typeof sandboxDataSchema>;

export const createSnapshotParamsSchema = z.object({
name: z.string().trim().min(1).max(64).optional(),
killSandboxAfter: z.boolean().optional(),
});
/** Parameters accepted when creating a snapshot from a sandbox. */
export type CreateSnapshotParams = z.infer<typeof createSnapshotParamsSchema>;

export const sandboxListItemSchema = z
.object({
id: z.string(),
Expand Down
12 changes: 3 additions & 9 deletions src/models/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,6 @@ export const snapshotDataSchema = z
/** Snapshot resource returned by the control plane API. */
export type SnapshotData = z.infer<typeof snapshotDataSchema>;

export const createSnapshotParamsSchema = z.object({
name: snapshotNameSchema.optional(),
});
/** Parameters accepted when creating or naming a snapshot. */
export type CreateSnapshotParams = z.infer<typeof createSnapshotParamsSchema>;

export const listSnapshotsParamsSchema = z.object({
query: z.string().max(64).optional(),
sort: z.enum(["created_at", "template_id"]).optional(),
Expand All @@ -44,11 +38,11 @@ export const listSnapshotsResponseSchema = z
/** Paginated snapshot list response. */
export type ListSnapshotsResponse = z.infer<typeof listSnapshotsResponseSchema>;

export const resumeSnapshotParamsSchema = z.object({
export const restoreSnapshotParamsSchema = z.object({
snapshotName: snapshotNameSchema,
autoPause: z.boolean().optional(),
timeout: z.number().int().min(1).max(28800).optional(),
networkPolicy: networkPolicySchema.optional(),
});
/** Parameters accepted when resuming a snapshot into a new sandbox. */
export type ResumeSnapshotParams = z.infer<typeof resumeSnapshotParamsSchema>;
/** Parameters accepted when restoring a snapshot into a new sandbox. */
export type RestoreSnapshotParams = z.infer<typeof restoreSnapshotParamsSchema>;
39 changes: 39 additions & 0 deletions src/services/sandboxes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS } from "@/confi
import { Leap0Error } from "@/core/errors.js";
import { normalize } from "@/core/normalize.js";
import {
createSnapshotParamsSchema,
createPresignedUrlParamsSchema,
createSandboxRuntimeParamsSchema,
listSandboxesParamsSchema,
Expand All @@ -15,9 +16,11 @@ import {
toObjectStorageMountUpdateWire,
toNetworkPolicyWire,
} from "@/models/sandbox.js";
import { snapshotDataSchema } from "@/models/snapshot.js";
import type {
CreatePresignedUrlParams,
CreateSandboxParams,
CreateSnapshotParams,
ListSandboxesParams,
ListSandboxesResponse,
ObjectStorageMount,
Expand All @@ -27,6 +30,7 @@ import type {
RequestOptions,
SandboxData,
SandboxRef,
SnapshotData,
} from "@/models/index.js";
import { Leap0Transport, jsonBody } from "@/core/transport.js";
import {
Expand Down Expand Up @@ -185,6 +189,41 @@ export class SandboxesClient<T = SandboxData> {
});
}

/**
* Creates a snapshot from a running sandbox.
*
* @param sandbox Sandbox ID or sandbox-like object.
* @param params Optional snapshot naming parameters.
* @param options Optional request settings such as timeout and query params.
* @returns The created snapshot resource.
*/
async createSnapshot(
sandbox: SandboxRef,
params: CreateSnapshotParams = {},
options: RequestOptions = {},
): Promise<SnapshotData> {
const parsedParams = createSnapshotParamsSchema.safeParse(params);
if (!parsedParams.success) {
throw new Leap0Error(parsedParams.error.issues[0]?.message ?? "Invalid snapshot parameters");
}

return withErrorPrefix("Failed to create snapshot: ", async () => {
const parsed = parsedParams.data;
const data = await this.transport.requestJson<unknown>(
`/v1/sandbox/${sandboxIdOf(sandbox)}/snapshot/create`,
{
method: "POST",
body: jsonBody({
name: parsed.name,
kill_sandbox_after: parsed.killSandboxAfter,
}),
},
options,
);
return normalize(snapshotDataSchema, data);
});
}

/**
* Fetches a sandbox by ID.
*
Expand Down
69 changes: 8 additions & 61 deletions src/services/snapshots.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,24 @@
import type {
CreateSnapshotParams,
ListSnapshotsParams,
ListSnapshotsResponse,
RequestOptions,
ResumeSnapshotParams,
RestoreSnapshotParams,
SandboxData,
SandboxRef,
SnapshotData,
SnapshotRef,
} from "@/models/index.js";
import { normalize } from "@/core/normalize.js";
import {
createSnapshotParamsSchema,
listSnapshotsParamsSchema,
listSnapshotsResponseSchema,
resumeSnapshotParamsSchema,
snapshotDataSchema,
restoreSnapshotParamsSchema,
} from "@/models/snapshot.js";
import { sandboxDataSchema, toNetworkPolicyWire } from "@/models/sandbox.js";
import { Leap0Transport, jsonBody } from "@/core/transport.js";
import { sandboxIdOf, snapshotIdOf } from "@/core/utils.js";
import { snapshotIdOf } from "@/core/utils.js";
import { withErrorPrefix } from "@/services/shared.js";

/**
* Creates, restores, and deletes named snapshots.
* Lists, restores, and deletes named snapshots.
*
* @throws {Leap0Error} If request validation, API calls, or response validation fail.
*/
Expand Down Expand Up @@ -69,66 +64,18 @@ export class SnapshotsClient<T = SandboxData> {
});
}

/**
* Creates a snapshot from a running sandbox.
*
* @param sandbox Sandbox ID or sandbox-like object.
* @param params Optional snapshot naming parameters.
* @param options Optional request settings such as timeout and query params.
* @returns The created snapshot resource.
*/
async create(
sandbox: SandboxRef,
params: CreateSnapshotParams = {},
options: RequestOptions = {},
): Promise<SnapshotData> {
return withErrorPrefix("Failed to create snapshot: ", async () => {
const parsed = createSnapshotParamsSchema.parse(params);
const data = await this.transport.requestJson<unknown>(
`/v1/sandbox/${sandboxIdOf(sandbox)}/snapshot/create`,
{ method: "POST", body: jsonBody(parsed) },
options,
);
return normalize(snapshotDataSchema, data);
});
}

/**
* Creates a snapshot and terminates the source sandbox.
*
* @param sandbox Sandbox ID or sandbox-like object.
* @param params Optional snapshot naming parameters.
* @param options Optional request settings such as timeout and query params.
* @returns The created snapshot resource.
*/
async pause(
sandbox: SandboxRef,
params: CreateSnapshotParams = {},
options: RequestOptions = {},
): Promise<SnapshotData> {
return withErrorPrefix("Failed to pause sandbox into snapshot: ", async () => {
const parsed = createSnapshotParamsSchema.parse(params);
const data = await this.transport.requestJson<unknown>(
`/v1/sandbox/${sandboxIdOf(sandbox)}/snapshot/pause`,
{ method: "POST", body: jsonBody(parsed) },
options,
);
return normalize(snapshotDataSchema, data);
});
}

/**
* Restores a sandbox from a snapshot.
*
* @param params Snapshot name and optional sandbox overrides.
* @param options Optional request settings such as timeout and query params.
* @returns The restored sandbox, optionally wrapped in a custom sandbox type.
*/
async resume(params: ResumeSnapshotParams, options: RequestOptions = {}): Promise<T> {
return withErrorPrefix("Failed to resume snapshot: ", async () => {
const parsed = resumeSnapshotParamsSchema.parse(params);
async restore(params: RestoreSnapshotParams, options: RequestOptions = {}): Promise<T> {
return withErrorPrefix("Failed to restore snapshot: ", async () => {
const parsed = restoreSnapshotParamsSchema.parse(params);
const data = await this.transport.requestJson<unknown>(
"/v1/snapshot/resume",
"/v1/snapshot/restore",
{
method: "POST",
body: jsonBody({
Expand Down
14 changes: 12 additions & 2 deletions tests/client/client-sandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,15 @@ test("Sandbox binds service methods to itself", async () => {
autoPause: false,
createdAt: "2026-01-01T00:00:00Z",
}),
createSnapshot: async () => ({
id: "snap-1",
name: "snap-a",
templateId: "tpl-1",
vcpu: 1,
memory: 1024,
disk: 4096,
createdAt: "2026-01-01T00:00:00Z",
}),
delete: async () => undefined,
addMount: async () => ({
id: "mnt-1",
Expand Down Expand Up @@ -148,6 +157,7 @@ test("Sandbox binds service methods to itself", async () => {
assert.equal(sandbox.memory, 2048);
await sandbox.pause();
assert.equal(sandbox.state, "paused");
assert.equal((await sandbox.createSnapshot({ name: "snap-a", killSandboxAfter: true })).id, "snap-1");
assert.equal((await sandbox.addMount({ type: "object-storage", bucket: "project-assets", mountPath: "/data/assets", endpoint: "https://storage.example.com" })).id, "mnt-1");
assert.equal((await sandbox.updateMount("mnt-1", { readOnly: false })).readOnly, false);
await sandbox.deleteMount("mnt-1");
Expand Down Expand Up @@ -204,7 +214,7 @@ test("client and sandbox helpers stay strongly typed", () => {
expectTypeOf<ReturnType<Leap0Client["sandboxes"]["create"]>>().toMatchTypeOf<
Promise<{ id: string }>
>();
expectTypeOf<ReturnType<Leap0Client["snapshots"]["resume"]>>().toMatchTypeOf<
expectTypeOf<ReturnType<Leap0Client["snapshots"]["restore"]>>().toMatchTypeOf<
Promise<{ id: string }>
>();

Expand All @@ -230,7 +240,7 @@ test("client and sandbox helpers stay strongly typed", () => {
| Array<{ id: string; type: "object-storage"; bucket: string; mountPath: string; prefix?: string | undefined; readOnly?: boolean | undefined }>
| undefined
>();
expectTypeOf<ReturnType<Sandbox["addMount"]>>().toEqualTypeOf<Promise<{ id: string; bucket: string }>>();
expectTypeOf<ReturnType<Sandbox["addMount"]>>().toEqualTypeOf<Promise<{ id: string; type: "object-storage"; bucket: string; mountPath: string; prefix?: string | undefined; readOnly?: boolean | undefined }>>();
expectTypeOf<Sandbox["updateMount"]>().parameters.toEqualTypeOf<
[mountID: string, mount: { bucket?: string; mountPath?: string; endpoint?: string; prefix?: string; readOnly?: boolean; accessKeyId?: string; secretAccessKey?: string }, options?: { timeout?: number }]
>();
Expand Down
30 changes: 30 additions & 0 deletions tests/services/sandboxes-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,36 @@ test("sandboxes get pause and delete target sandbox ids", async () => {
assert.equal(calls[2]?.path, "/v1/sandbox/sb-3/");
});

test("sandboxes createSnapshot targets sandbox snapshot endpoint", async () => {
const { transport, calls } = createRecordedTransport({
requestJson: (path: string, init: RequestInit, options: unknown) => {
calls.push({ path, init, options: options as never });
return Promise.resolve({
id: "snap-1",
name: "snap-a",
template_id: "tpl-1",
vcpu: 2,
memory: 1024,
disk: 4096,
created_at: "2026-01-01T00:00:00Z",
});
},
});
const client = new SandboxesClient(transport as never);

const created = await client.createSnapshot("sb-1", { name: "snap-a", killSandboxAfter: true });

assert.equal(created.id, "snap-1");
assert.equal(calls[0]?.path, "/v1/sandbox/sb-1/snapshot/create");
assert.deepEqual(jsonOf(calls[0]!), { name: "snap-a", kill_sandbox_after: true });
});

test("sandboxes createSnapshot validates snapshot params", async () => {
const { client } = makeClient();
await assert.rejects(() => client.createSnapshot("sb-1", { name: "" }), Leap0Error);
await assert.rejects(() => client.createSnapshot("sb-1", { name: " " }), Leap0Error);
});

test("sandboxes runtime info targets system endpoints", async () => {
const { transport, calls } = createRecordedTransport({
requestJson: (path: string, init: RequestInit, options: unknown) => {
Expand Down
Loading