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
21 changes: 21 additions & 0 deletions src/client/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,4 +230,25 @@ export class Sandbox implements SandboxData {
websocketUrl(path = "/", port?: number): string {
return this.client.sandboxes.websocketUrl(this.id, path, port);
}

/**
* Fetches the resolved home directory for the sandbox user.
*
* @param options Optional request settings.
* @returns The resolved sandbox user home directory.
*/
async getUserHomeDir(options?: { timeout?: number }): Promise<string> {
return this.client.sandboxes.getUserHomeDir(this.id, options);
}

/**
* Fetches the configured working directory for the sandbox.
*
* @param options Optional request settings.
* @returns The configured sandbox workdir.
*/
async getWorkdir(options?: { timeout?: number }): Promise<string> {
return this.client.sandboxes.getWorkdir(this.id, options);
}

}
10 changes: 10 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ export {
export { Leap0Client, Sandbox } from "@/client/index.js";
export {
CodeLanguage,
codeLanguageSchema,
listSandboxesParamsSchema,
listSandboxesResponseSchema,
listSnapshotsParamsSchema,
listSnapshotsResponseSchema,
NetworkPolicyMode,
RegistryCredentialType,
SandboxState,
Expand Down Expand Up @@ -44,7 +49,11 @@ export type {
CodeExecutionResult,
CreatePtySessionParams,
CreateSandboxParams,
ListSandboxesParams,
ListSandboxesResponse,
CreateSnapshotParams,
ListSnapshotsParams,
ListSnapshotsResponse,
CreateTemplateParams,
DesktopDisplayInfo,
DesktopHealth,
Expand Down Expand Up @@ -73,6 +82,7 @@ export type {
ProcessResult,
PtySession,
ResumeSnapshotParams,
SandboxListItem,
SearchMatch,
SshAccess,
SshValidation,
Expand Down
31 changes: 27 additions & 4 deletions src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,31 @@ export type { RequestOptions } from "@/models/shared.js";

export type { SandboxRef, SnapshotRef, TemplateRef } from "@/models/refs.js";

export { createSandboxParamsSchema, NetworkPolicyMode, SandboxState } from "@/models/sandbox.js";
export type { CreateSandboxParams, NetworkPolicy, SandboxData } from "@/models/sandbox.js";
export {
createSandboxParamsSchema,
listSandboxesParamsSchema,
listSandboxesResponseSchema,
NetworkPolicyMode,
SandboxState,
} from "@/models/sandbox.js";
Comment thread
coderabbitai[bot] marked this conversation as resolved.
export type {
CreateSandboxParams,
ListSandboxesParams,
ListSandboxesResponse,
NetworkPolicy,
SandboxData,
SandboxListItem,
} from "@/models/sandbox.js";

export { createSnapshotParamsSchema } from "@/models/snapshot.js";
export {
createSnapshotParamsSchema,
listSnapshotsParamsSchema,
listSnapshotsResponseSchema,
} from "@/models/snapshot.js";
export type {
CreateSnapshotParams,
ListSnapshotsParams,
ListSnapshotsResponse,
ResumeSnapshotParams,
SnapshotData,
} from "@/models/snapshot.js";
Expand Down Expand Up @@ -48,7 +67,11 @@ export type { LspJsonRpcError, LspJsonRpcResponse, LspResponse } from "@/models/

export type { SshAccess, SshValidation } from "@/models/ssh.js";

export { CodeLanguage, codeLanguageSchema, StreamEventType } from "@/models/code-interpreter.js";
export {
CodeLanguage,
codeLanguageSchema,
StreamEventType,
} from "@/models/code-interpreter.js";
export type {
CodeContext,
CodeExecutionError,
Expand Down
45 changes: 45 additions & 0 deletions src/models/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,51 @@ export const sandboxDataSchema = z
/** Sandbox resource returned by the control plane API. */
export type SandboxData = z.infer<typeof sandboxDataSchema>;

export const sandboxListItemSchema = z
.object({
id: z.string(),
templateId: z.string(),
podId: z.string(),
state: sandboxStateSchema,
launchTime: z.string().optional(),
stateChangeTime: z.string().optional(),
timeoutAt: z.number().optional(),
createdAt: z.string(),
})
.catchall(z.unknown());
/** Sandbox summary returned by the list sandboxes API. */
export type SandboxListItem = z.infer<typeof sandboxListItemSchema>;

export const listSandboxesResponseSchema = z
.object({
items: z.array(sandboxListItemSchema),
totalItems: z.number().int().nonnegative(),
})
.catchall(z.unknown());
/** Paginated sandbox list response. */
export type ListSandboxesResponse = z.infer<typeof listSandboxesResponseSchema>;

export const listSandboxesParamsSchema = z
.object({
state: z
.enum([
SandboxState.STARTING,
SandboxState.SNAPSHOTTING,
SandboxState.RUNNING,
SandboxState.PAUSED,
SandboxState.UNPAUSING,
SandboxState.DELETING,
])
.optional(),
sort: z.enum(["created_at", "state"]).optional(),
orderBy: z.enum(["asc", "desc"]).optional(),
page: z.number().int().min(1).optional(),
pageSize: z.number().int().min(1).max(100).optional(),
})
.passthrough();
/** Parameters accepted when listing sandboxes. */
export type ListSandboxesParams = z.infer<typeof listSandboxesParamsSchema>;

export const createSandboxParamsSchema = z.object({
templateName: z.string().optional(),
vcpu: z.number().int().positive().optional(),
Expand Down
19 changes: 19 additions & 0 deletions src/models/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,25 @@ export const createSnapshotParamsSchema = z.object({
/** 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(),
orderBy: z.enum(["asc", "desc"]).optional(),
page: z.number().int().min(1).optional(),
pageSize: z.number().int().min(1).max(100).optional(),
});
/** Parameters accepted when listing snapshots. */
export type ListSnapshotsParams = z.infer<typeof listSnapshotsParamsSchema>;

export const listSnapshotsResponseSchema = z
.object({
items: z.array(snapshotDataSchema),
totalItems: z.number().int().nonnegative(),
})
.catchall(z.unknown());
/** Paginated snapshot list response. */
export type ListSnapshotsResponse = z.infer<typeof listSnapshotsResponseSchema>;

export const resumeSnapshotParamsSchema = z.object({
snapshotName: snapshotNameSchema,
autoPause: z.boolean().optional(),
Expand Down
87 changes: 87 additions & 0 deletions src/services/sandboxes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ import { Leap0Error } from "@/core/errors.js";
import { normalize } from "@/core/normalize.js";
import {
createSandboxRuntimeParamsSchema,
listSandboxesParamsSchema,
listSandboxesResponseSchema,
sandboxDataSchema,
toNetworkPolicyWire,
} from "@/models/sandbox.js";
import type {
CreateSandboxParams,
ListSandboxesParams,
ListSandboxesResponse,
RequestOptions,
SandboxData,
SandboxRef,
Expand Down Expand Up @@ -118,6 +122,38 @@ export class SandboxesClient<T = SandboxData> {
});
}

/**
* Lists sandboxes for the authenticated organization.
*
* @param params Optional filter, sort, and pagination parameters.
* @param options Optional request settings such as timeout and headers.
* @returns Paginated sandbox summaries.
*/
async list(
params: ListSandboxesParams = {},
options: RequestOptions = {},
): Promise<ListSandboxesResponse> {
return withErrorPrefix("Failed to list sandboxes: ", async () => {
const parsed = listSandboxesParamsSchema.parse(params);
const data = await this.transport.requestJson<unknown>(
"/v1/sandboxes",
{ method: "GET" },
{
...options,
query: {
...options.query,
state: parsed.state,
sort: parsed.sort,
"order-by": parsed.orderBy,
page: parsed.page,
"page-size": parsed.pageSize,
},
},
);
return normalize(listSandboxesResponseSchema, data);
});
}

/**
* Pauses a running sandbox.
*
Expand Down Expand Up @@ -166,6 +202,57 @@ export class SandboxesClient<T = SandboxData> {
);
}

/**
* Fetches the resolved home directory for the sandbox user.
*
* @param sandbox Sandbox ID or sandbox-like object.
* @param options Optional request settings such as timeout and query params.
* @returns The resolved sandbox user home directory.
*/
async getUserHomeDir(sandbox: SandboxRef, options: RequestOptions = {}): Promise<string> {
return withErrorPrefix("Failed to get sandbox user home directory: ", async () => {
const data = await this.transport.requestJson<unknown>(
`/v1/sandbox/${sandboxIdOf(sandbox)}/system/user-home-dir`,
{ method: "GET" },
options,
);
if (
typeof data !== "object" ||
data === null ||
typeof (data as { user_home_dir?: unknown }).user_home_dir !== "string"
) {
throw new Leap0Error("Sandbox user home directory response missing user_home_dir");
}
return (data as { user_home_dir: string }).user_home_dir;
});
}

/**
* Fetches the configured working directory for the sandbox.
*
* @param sandbox Sandbox ID or sandbox-like object.
* @param options Optional request settings such as timeout and query params.
* @returns The configured sandbox workdir.
*/
async getWorkdir(sandbox: SandboxRef, options: RequestOptions = {}): Promise<string> {
return withErrorPrefix("Failed to get sandbox workdir: ", async () => {
const data = await this.transport.requestJson<unknown>(
`/v1/sandbox/${sandboxIdOf(sandbox)}/system/workdir`,
{ method: "GET" },
options,
);
if (
typeof data !== "object" ||
data === null ||
typeof (data as { workdir?: unknown }).workdir !== "string"
) {
throw new Leap0Error("Sandbox workdir response missing workdir");
}
return (data as { workdir: string }).workdir;
});
}


/**
* Builds the public invoke URL for a sandbox.
*
Expand Down
36 changes: 36 additions & 0 deletions src/services/snapshots.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type {
CreateSnapshotParams,
ListSnapshotsParams,
ListSnapshotsResponse,
RequestOptions,
ResumeSnapshotParams,
SandboxData,
Expand All @@ -10,6 +12,8 @@ import type {
import { normalize } from "@/core/normalize.js";
import {
createSnapshotParamsSchema,
listSnapshotsParamsSchema,
listSnapshotsResponseSchema,
resumeSnapshotParamsSchema,
snapshotDataSchema,
} from "@/models/snapshot.js";
Expand All @@ -33,6 +37,38 @@ export class SnapshotsClient<T = SandboxData> {
return (this.sandboxFactory ? this.sandboxFactory(data) : data) as T;
}

/**
* Lists snapshots for the authenticated organization.
*
* @param params Optional filter, sort, and pagination parameters.
* @param options Optional request settings such as timeout and headers.
* @returns Paginated snapshot summaries.
*/
async list(
params: ListSnapshotsParams = {},
options: RequestOptions = {},
): Promise<ListSnapshotsResponse> {
return withErrorPrefix("Failed to list snapshots: ", async () => {
const parsed = listSnapshotsParamsSchema.parse(params);
const data = await this.transport.requestJson<unknown>(
"/v1/snapshots",
{ method: "GET" },
{
...options,
query: {
...options.query,
query: parsed.query,
sort: parsed.sort,
"order-by": parsed.orderBy,
page: parsed.page,
"page-size": parsed.pageSize,
},
},
);
return normalize(listSnapshotsResponseSchema, data);
});
}

/**
* Creates a snapshot from a running sandbox.
*
Expand Down
6 changes: 6 additions & 0 deletions tests/client/client-sandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ test("Sandbox binds service methods to itself", async () => {
createdAt: "2026-01-01T00:00:00Z",
}),
delete: async () => undefined,
getUserHomeDir: async (id: string) => `home:${id}`,
getWorkdir: async (id: string) => `workdir:${id}`,
invokeUrl: (id: string, path: string, port?: number) => `invoke:${id}:${path}:${port ?? ""}`,
websocketUrl: (id: string, path: string, port?: number) => `ws:${id}:${path}:${port ?? ""}`,
},
Expand Down Expand Up @@ -120,6 +122,8 @@ test("Sandbox binds service methods to itself", async () => {
await sandbox.pause();
assert.equal(sandbox.state, "paused");
assert.equal(sandbox.invokeUrl("/healthz", 3000), "invoke:sb-1:/healthz:3000");
assert.equal(await sandbox.getUserHomeDir(), "home:sb-1");
assert.equal(await sandbox.getWorkdir(), "workdir:sb-1");
});

test("Sandbox refresh rejects invalid sandbox states", async () => {
Expand Down Expand Up @@ -178,6 +182,8 @@ test("client and sandbox helpers stay strongly typed", () => {
expectTypeOf<Sandbox["ssh"]["validateAccess"]>().parameters.toEqualTypeOf<
[accessId: string, password: string, options?: RequestOptions]
>();
expectTypeOf<ReturnType<Sandbox["getUserHomeDir"]>>().toEqualTypeOf<Promise<string>>();
expectTypeOf<ReturnType<Sandbox["getWorkdir"]>>().toEqualTypeOf<Promise<string>>();
expectTypeOf<Sandbox["templateName"]>().toEqualTypeOf<string | undefined>();
expectTypeOf<Sandbox["timeoutMin"]>().toEqualTypeOf<number | undefined>();
expectTypeOf<Sandbox["envVars"]>().toEqualTypeOf<Record<string, string> | undefined>();
Expand Down
Loading