/**
 * Main SandboxClient class for interacting with the sandbox server API.
 */
import { getLangSmithEnvironmentVariable } from "../utils/env.js";
import { _getFetchImplementation } from "../singletons/fetch.js";
import { AsyncCaller } from "../utils/async_caller.js";
import { Langsmith as OpenAPILangsmith } from "../_openapi_client/index.js";
import { Sandbox } from "./sandbox.js";
import { LangSmithResourceCreationError, LangSmithResourceNameConflictError, LangSmithResourceNotFoundError, LangSmithResourceTimeoutError, LangSmithSandboxAPIError, LangSmithValidationError, } from "./errors.js";
import { handleClientHttpError, handleSandboxCreationError, validateTtl, } from "./helpers.js";
import { validateMountConfigProxyConfig } from "./mounts.js";
import { v4 as uuidv4 } from "../utils/uuid/src/index.js";
/**
 * Sleep that can be interrupted by an AbortSignal.
 * Resolves after `ms` milliseconds or rejects immediately if the signal fires.
 */
function sleepWithSignal(ms, signal) {
    if (!signal) {
        return new Promise((resolve) => setTimeout(resolve, ms));
    }
    const abortSignal = signal;
    abortSignal.throwIfAborted();
    return new Promise((resolve, reject) => {
        const timer = setTimeout(() => {
            abortSignal.removeEventListener("abort", onAbort);
            resolve();
        }, ms);
        function onAbort() {
            clearTimeout(timer);
            reject(abortSignal.reason);
        }
        abortSignal.addEventListener("abort", onAbort, { once: true });
    });
}
/**
 * Get the default sandbox API endpoint from environment.
 *
 * Derives the endpoint from LANGSMITH_ENDPOINT (or LANGCHAIN_ENDPOINT).
 */
function getDefaultApiEndpoint() {
    const base = getLangSmithEnvironmentVariable("ENDPOINT") ??
        "https://api.smith.langchain.com";
    return `${base.replace(/\/$/, "")}/v2/sandboxes`;
}
/**
 * Get the default API key from environment.
 */
function getDefaultApiKey() {
    return getLangSmithEnvironmentVariable("API_KEY");
}
function shellQuote(value) {
    return `'${value.replace(/'/g, "'\\''")}'`;
}
function writeString(header, value, offset, length) {
    header.write(value.slice(0, length), offset, length, "utf8");
}
function writeOctal(header, value, offset, length) {
    const octal = value.toString(8).padStart(length - 1, "0");
    header.write(octal.slice(-length + 1) + "\0", offset, length, "ascii");
}
function splitTarPath(name) {
    if (Buffer.byteLength(name) <= 100) {
        return { name, prefix: "" };
    }
    const parts = name.split("/");
    for (let i = 1; i < parts.length; i += 1) {
        const prefix = parts.slice(0, i).join("/");
        const basename = parts.slice(i).join("/");
        if (Buffer.byteLength(prefix) <= 155 &&
            Buffer.byteLength(basename) <= 100) {
            return { name: basename, prefix };
        }
    }
    throw new Error(`Docker build context path is too long for tar: ${name}`);
}
function makeTarHeader(args) {
    const header = Buffer.alloc(512, 0);
    const split = splitTarPath(args.name);
    writeString(header, split.name, 0, 100);
    writeOctal(header, args.mode, 100, 8);
    writeOctal(header, 0, 108, 8);
    writeOctal(header, 0, 116, 8);
    writeOctal(header, args.size, 124, 12);
    writeOctal(header, Math.floor(args.mtimeMs / 1000), 136, 12);
    header.fill(" ", 148, 156);
    writeString(header, args.type === "directory" ? "5" : args.type === "symlink" ? "2" : "0", 156, 1);
    if (args.linkName) {
        writeString(header, args.linkName, 157, 100);
    }
    writeString(header, "ustar", 257, 6);
    writeString(header, "00", 263, 2);
    if (split.prefix) {
        writeString(header, split.prefix, 345, 155);
    }
    let checksum = 0;
    for (const byte of header) {
        checksum += byte;
    }
    header.write(checksum.toString(8).padStart(6, "0") + "\0 ", 148, 8, "ascii");
    return header;
}
async function makeDockerContextTar(contextPath) {
    const fs = await import("node:fs/promises");
    const path = await import("node:path");
    const contextRoot = path.resolve(contextPath);
    const chunks = [];
    async function addEntry(absPath) {
        const rel = path.relative(contextRoot, absPath);
        if (!rel || rel.split(path.sep).includes(".git")) {
            return;
        }
        const tarPath = rel.split(path.sep).join("/");
        const stat = await fs.lstat(absPath);
        if (stat.isDirectory()) {
            chunks.push(makeTarHeader({
                name: tarPath.endsWith("/") ? tarPath : `${tarPath}/`,
                mode: stat.mode & 0o777,
                size: 0,
                type: "directory",
                mtimeMs: stat.mtimeMs,
            }));
            const entries = await fs.readdir(absPath);
            for (const entry of entries.sort()) {
                await addEntry(path.join(absPath, entry));
            }
            return;
        }
        if (stat.isSymbolicLink()) {
            chunks.push(makeTarHeader({
                name: tarPath,
                mode: stat.mode & 0o777,
                size: 0,
                type: "symlink",
                linkName: await fs.readlink(absPath),
                mtimeMs: stat.mtimeMs,
            }));
            return;
        }
        if (!stat.isFile()) {
            return;
        }
        const content = await fs.readFile(absPath);
        chunks.push(makeTarHeader({
            name: tarPath,
            mode: stat.mode & 0o777,
            size: content.byteLength,
            type: "file",
            mtimeMs: stat.mtimeMs,
        }), content);
        const padding = (512 - (content.byteLength % 512)) % 512;
        if (padding) {
            chunks.push(Buffer.alloc(padding, 0));
        }
    }
    const rootEntries = await fs.readdir(contextRoot);
    for (const entry of rootEntries.sort()) {
        await addEntry(path.join(contextRoot, entry));
    }
    chunks.push(Buffer.alloc(1024, 0));
    return new Uint8Array(Buffer.concat(chunks));
}
async function resolveDockerfileContext(dockerfile, context) {
    const fs = await import("node:fs/promises");
    const path = await import("node:path");
    const contextPath = path.resolve(context);
    const dockerfilePath = path.resolve(contextPath, dockerfile);
    const contextStat = await fs.stat(contextPath);
    if (!contextStat.isDirectory()) {
        throw new Error(`context must be a directory: ${contextPath}`);
    }
    const dockerfileStat = await fs.stat(dockerfilePath);
    if (!dockerfileStat.isFile()) {
        throw new Error(`dockerfile must be a file: ${dockerfilePath}`);
    }
    const dockerfileRel = path.relative(contextPath, dockerfilePath);
    if (dockerfileRel === "" ||
        dockerfileRel.startsWith("..") ||
        path.isAbsolute(dockerfileRel)) {
        throw new Error("dockerfile must be inside context");
    }
    return {
        contextPath,
        dockerfileRel: dockerfileRel.split(path.sep).join("/"),
    };
}
function makeDockerfileBuildCommand(args) {
    const dockerfileRemote = `${args.remoteContext}/${args.dockerfileRel}`;
    const dockerfileDir = dockerfileRemote.split("/").slice(0, -1).join("/");
    const dockerfileName = dockerfileRemote.split("/").at(-1) ?? "Dockerfile";
    const socketPath = `${args.buildkitRun}/buildkitd.sock`;
    const buildctl = [
        "buildctl",
        "--addr",
        `unix://${socketPath}`,
        "build",
        "--progress=plain",
        "--frontend",
        "dockerfile.v0",
        "--local",
        `context=${args.remoteContext}`,
        "--local",
        `dockerfile=${dockerfileDir}`,
        "--opt",
        `filename=${dockerfileName}`,
        "--output",
        `type=docker,name=${args.imageRef}`,
    ];
    if (args.target !== undefined) {
        buildctl.push("--opt", `target=${args.target}`);
    }
    for (const [key, value] of Object.entries(args.buildArgs ?? {}).sort()) {
        buildctl.push("--opt", `build-arg:${key}=${value}`);
    }
    return [
        "set -euo pipefail",
        `mkdir -p ${shellQuote(args.buildkitRoot)} ${shellQuote(args.buildkitRun)}`,
        `buildkitd --addr ${shellQuote(`unix://${socketPath}`)} --root ${shellQuote(args.buildkitRoot)} --oci-worker=true --containerd-worker=false --oci-worker-snapshotter=native --oci-worker-binary buildkit-runc > ${shellQuote(`${args.buildkitRun}/buildkitd.log`)} 2>&1 &`,
        "buildkitd_pid=$!",
        'cleanup() { kill "$buildkitd_pid" >/dev/null 2>&1 || true; }',
        "trap cleanup EXIT",
        "for i in $(seq 1 300); do",
        `  if buildctl --addr ${shellQuote(`unix://${socketPath}`)} debug workers >/dev/null 2>&1; then break; fi`,
        `  if ! kill -0 "$buildkitd_pid" >/dev/null 2>&1; then cat ${shellQuote(`${args.buildkitRun}/buildkitd.log`)}; exit 1; fi`,
        `  if [ "$i" = 300 ]; then cat ${shellQuote(`${args.buildkitRun}/buildkitd.log`)}; exit 1; fi`,
        "  sleep 0.1",
        "done",
        "for i in $(seq 1 300); do",
        "  if docker info >/dev/null 2>&1; then break; fi",
        '  if [ "$i" = 300 ]; then docker info; exit 1; fi',
        "  sleep 0.1",
        "done",
        `${buildctl.map(shellQuote).join(" ")} | docker load`,
        `rm -rf ${shellQuote(args.buildkitRoot)} || true`,
        "",
    ].join("\n");
}
/**
 * Client for interacting with the Sandbox Server API.
 *
 * This client provides a simple interface for managing sandboxes and snapshots.
 *
 * @example
 * ```typescript
 * import { SandboxClient } from "langsmith/sandbox";
 *
 * // Uses LANGSMITH_ENDPOINT and LANGSMITH_API_KEY from environment
 * const client = new SandboxClient();
 *
 * // Or with explicit configuration
 * const client = new SandboxClient({
 *   apiEndpoint: "https://api.smith.langchain.com/v2/sandboxes",
 *   apiKey: "your-api-key",
 * });
 *
 * // Create a sandbox with the default runtime
 * const sandbox = await client.createSandbox();
 * try {
 *   const result = await sandbox.run("python --version");
 *   console.log(result.stdout);
 * } finally {
 *   await sandbox.delete();
 * }
 * ```
 */
export class SandboxClient {
    constructor(config = {}) {
        Object.defineProperty(this, "_baseUrl", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "_apiKey", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "_defaultHeaders", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "_fetchImpl", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "_caller", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "_registriesClient", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        this._baseUrl = (config.apiEndpoint ?? getDefaultApiEndpoint()).replace(/\/$/, "");
        this._apiKey = config.apiKey ?? getDefaultApiKey();
        this._defaultHeaders = { ...(config.headers ?? {}) };
        this._fetchImpl = _getFetchImplementation();
        this._caller = new AsyncCaller({
            maxRetries: config.maxRetries ?? 3,
            maxConcurrency: config.maxConcurrency ?? Infinity,
        });
    }
    _apiRoot() {
        const suffix = "/v2/sandboxes";
        return this._baseUrl.endsWith(suffix)
            ? this._baseUrl.slice(0, -suffix.length)
            : this._baseUrl;
    }
    /**
     * Manage sandbox image registries: create, list, retrieve, update, delete.
     *
     * A registry stores credentials for pulling private images. Create one, then
     * pass its `id` as `registryId` when building a snapshot.
     *
     * @example
     * ```typescript
     * const registry = await client.registries.create({
     *   name: "internal",
     *   url: "registry.example.com",
     *   username: "robot",
     *   password: process.env.REGISTRY_PASSWORD,
     * });
     * const snapshot = await client.createSnapshot(
     *   "internal-python",
     *   "registry.example.com/internal/python:3.12",
     *   2_147_483_648,
     *   { registryId: registry.id },
     * );
     * ```
     */
    get registries() {
        if (!this._registriesClient) {
            const defaultHeaders = {
                ...this._defaultHeaders,
            };
            if (this._apiKey === undefined) {
                defaultHeaders["X-API-Key"] = null;
            }
            this._registriesClient = new OpenAPILangsmith({
                apiKey: this._apiKey,
                baseURL: this._apiRoot(),
                defaultHeaders,
                fetch: this._fetchImpl,
            });
        }
        return this._registriesClient.sandboxes.registries;
    }
    /**
     * Internal fetch method that adds authentication headers.
     *
     * Uses AsyncCaller to handle retries for transient failures
     * (network errors, 5xx, 429).
     *
     * @internal
     */
    async _fetch(url, init = {}) {
        const headers = new Headers(init.headers);
        if (this._apiKey) {
            headers.set("X-Api-Key", this._apiKey);
        }
        for (const [name, value] of Object.entries(this._defaultHeaders)) {
            if (!headers.has(name)) {
                headers.set(name, value);
            }
        }
        return this._caller.call(() => this._fetchImpl(url, {
            ...init,
            headers,
        }));
    }
    /**
     * Get the API key for WebSocket authentication.
     * @internal
     */
    getApiKey() {
        return this._apiKey;
    }
    /**
     * Get the constructor-supplied default headers. Used by the WebSocket exec
     * path so headers like `X-Service-Key` set on the client are attached to
     * the WS upgrade request.
     * @internal
     */
    getDefaultHeaders() {
        return { ...this._defaultHeaders };
    }
    /**
     * JSON POST helper. Sends JSON body, checks response status,
     * and returns the Response for further processing.
     * Throws on non-ok responses via handleClientHttpError.
     * Callers can add specific status checks (e.g. 404) before calling this.
     * @internal
     */
    async _postJson(url, body, options) {
        const response = await this._fetch(url, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(body),
            signal: options?.signal,
        });
        if (!response.ok) {
            await handleClientHttpError(response);
        }
        return response;
    }
    async createSandbox(snapshotIdOrOptions, options = {}) {
        const snapshotId = typeof snapshotIdOrOptions === "string" ? snapshotIdOrOptions : undefined;
        const resolvedOptions = typeof snapshotIdOrOptions === "object" && snapshotIdOrOptions !== null
            ? snapshotIdOrOptions
            : options;
        const { snapshotName, name, timeout = 30, waitForReady = true, idleTtlSeconds, deleteAfterStopSeconds, vCpus, memBytes, fsCapacityBytes, mountConfig, proxyConfig, } = resolvedOptions;
        if (snapshotId && snapshotName) {
            throw new LangSmithValidationError("At most one of snapshotId or options.snapshotName may be set", "snapshotId");
        }
        validateTtl(idleTtlSeconds, "idleTtlSeconds");
        validateTtl(deleteAfterStopSeconds, "deleteAfterStopSeconds");
        const url = `${this._baseUrl}/boxes`;
        const payload = {
            wait_for_ready: waitForReady,
        };
        if (snapshotId) {
            payload.snapshot_id = snapshotId;
        }
        if (snapshotName) {
            payload.snapshot_name = snapshotName;
        }
        if (waitForReady) {
            payload.timeout = timeout;
        }
        if (name) {
            payload.name = name;
        }
        if (idleTtlSeconds !== undefined) {
            payload.idle_ttl_seconds = idleTtlSeconds;
        }
        if (deleteAfterStopSeconds !== undefined) {
            payload.delete_after_stop_seconds = deleteAfterStopSeconds;
        }
        if (vCpus !== undefined) {
            payload.vcpus = vCpus;
        }
        if (memBytes !== undefined) {
            payload.mem_bytes = memBytes;
        }
        if (fsCapacityBytes !== undefined) {
            payload.fs_capacity_bytes = fsCapacityBytes;
        }
        if (mountConfig !== undefined) {
            validateMountConfigProxyConfig(mountConfig, proxyConfig);
            payload.mount_config = mountConfig;
        }
        if (proxyConfig !== undefined) {
            payload.proxy_config = proxyConfig;
        }
        const httpTimeout = waitForReady ? (timeout + 30) * 1000 : 30 * 1000;
        const response = await this._fetch(url, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(payload),
            signal: AbortSignal.timeout(httpTimeout),
        });
        if (!response.ok) {
            await handleSandboxCreationError(response);
        }
        const data = (await response.json());
        return new Sandbox(data, this);
    }
    /**
     * Get a Sandbox by name.
     *
     * The sandbox is NOT automatically deleted. Use deleteSandbox() for cleanup.
     *
     * @param name - Sandbox name.
     * @returns Sandbox.
     * @throws LangSmithResourceNotFoundError if sandbox not found.
     */
    async getSandbox(name, options) {
        const url = `${this._baseUrl}/boxes/${encodeURIComponent(name)}`;
        const response = await this._fetch(url, { signal: options?.signal });
        if (!response.ok) {
            if (response.status === 404) {
                throw new LangSmithResourceNotFoundError(`Sandbox '${name}' not found`, "sandbox");
            }
            await handleClientHttpError(response);
        }
        const data = (await response.json());
        return new Sandbox(data, this);
    }
    /**
     * List all Sandboxes.
     *
     * @returns List of Sandboxes.
     */
    async listSandboxes() {
        const url = `${this._baseUrl}/boxes`;
        const response = await this._fetch(url);
        if (!response.ok) {
            if (response.status === 404) {
                throw new LangSmithSandboxAPIError(`API endpoint not found: ${url}. Check that apiEndpoint is correct.`);
            }
            await handleClientHttpError(response);
        }
        const data = await response.json();
        return (data.sandboxes ?? []).map((s) => new Sandbox(s, this));
    }
    async updateSandbox(name, newNameOrOptions) {
        const options = typeof newNameOrOptions === "string"
            ? { newName: newNameOrOptions }
            : newNameOrOptions;
        const { newName, idleTtlSeconds, deleteAfterStopSeconds } = options;
        validateTtl(idleTtlSeconds, "idleTtlSeconds");
        validateTtl(deleteAfterStopSeconds, "deleteAfterStopSeconds");
        if (newName === undefined &&
            idleTtlSeconds === undefined &&
            deleteAfterStopSeconds === undefined) {
            return this.getSandbox(name);
        }
        const url = `${this._baseUrl}/boxes/${encodeURIComponent(name)}`;
        const payload = {};
        if (newName !== undefined) {
            payload.name = newName;
        }
        if (idleTtlSeconds !== undefined) {
            payload.idle_ttl_seconds = idleTtlSeconds;
        }
        if (deleteAfterStopSeconds !== undefined) {
            payload.delete_after_stop_seconds = deleteAfterStopSeconds;
        }
        const response = await this._fetch(url, {
            method: "PATCH",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(payload),
        });
        if (!response.ok) {
            if (response.status === 404) {
                throw new LangSmithResourceNotFoundError(`Sandbox '${name}' not found`, "sandbox");
            }
            if (response.status === 409) {
                throw new LangSmithResourceNameConflictError(newName !== undefined
                    ? `Sandbox name '${newName}' already in use`
                    : "Sandbox update conflict (name may already be in use)", "sandbox");
            }
            await handleClientHttpError(response);
        }
        const data = (await response.json());
        return new Sandbox(data, this);
    }
    /**
     * Delete a Sandbox.
     *
     * @param name - Sandbox name.
     * @throws LangSmithResourceNotFoundError if sandbox not found.
     */
    async deleteSandbox(name) {
        const url = `${this._baseUrl}/boxes/${encodeURIComponent(name)}`;
        const response = await this._fetch(url, { method: "DELETE" });
        if (!response.ok) {
            if (response.status === 404) {
                throw new LangSmithResourceNotFoundError(`Sandbox '${name}' not found`, "sandbox");
            }
            await handleClientHttpError(response);
        }
    }
    /**
     * Get the provisioning status of a sandbox.
     *
     * This is a lightweight endpoint designed for polling during async creation.
     * Use this instead of getSandbox() when you only need the status.
     *
     * @param name - Sandbox name.
     * @returns ResourceStatus with status and optional status_message.
     * @throws LangSmithResourceNotFoundError if sandbox not found.
     */
    async getSandboxStatus(name, options) {
        const url = `${this._baseUrl}/boxes/${encodeURIComponent(name)}/status`;
        const response = await this._fetch(url, { signal: options?.signal });
        if (!response.ok) {
            if (response.status === 404) {
                throw new LangSmithResourceNotFoundError(`Sandbox '${name}' not found`, "sandbox");
            }
            await handleClientHttpError(response);
        }
        return (await response.json());
    }
    /**
     * Wait for a sandbox to become ready.
     *
     * Polls getSandboxStatus() until the sandbox reaches "ready" or "failed" status,
     * then returns the full Sandbox object.
     *
     * @param name - Sandbox name.
     * @param options - Polling options (timeout, pollInterval).
     * @returns Ready Sandbox.
     * @throws LangSmithResourceCreationError if sandbox status becomes "failed".
     * @throws LangSmithResourceTimeoutError if timeout expires while still provisioning.
     * @throws LangSmithResourceNotFoundError if sandbox not found.
     *
     * @example
     * ```typescript
     * const sandbox = await client.createSandbox(snapshot.id, { waitForReady: false });
     * // ... do other work ...
     * const readySandbox = await client.waitForSandbox(sandbox.name);
     * ```
     */
    async waitForSandbox(name, options = {}) {
        const { timeout = 120, pollInterval = 1.0, signal } = options;
        const deadline = Date.now() + timeout * 1000;
        let lastStatus = "provisioning";
        while (Date.now() < deadline) {
            signal?.throwIfAborted();
            const statusResult = await this.getSandboxStatus(name, { signal });
            lastStatus = statusResult.status;
            if (statusResult.status === "ready") {
                return this.getSandbox(name, { signal });
            }
            if (statusResult.status === "failed") {
                throw new LangSmithResourceCreationError(statusResult.status_message ?? `Sandbox '${name}' creation failed`, "sandbox");
            }
            // Wait before polling again, capped to remaining time + jitter
            const remaining = deadline - Date.now();
            const jitter = pollInterval * 200 * (Math.random() - 0.5); // ±10%
            const delay = Math.min(pollInterval * 1000 + jitter, remaining);
            if (delay > 0) {
                await sleepWithSignal(delay, signal);
            }
        }
        throw new LangSmithResourceTimeoutError(`Sandbox '${name}' did not become ready within ${timeout}s`, "sandbox", lastStatus);
    }
    /**
     * Start a stopped sandbox and wait until ready.
     *
     * @param name - Sandbox name.
     * @param options - Options with timeout.
     * @returns Sandbox in "ready" status.
     */
    async startSandbox(name, options = {}) {
        const { timeout = 120, signal } = options;
        const url = `${this._baseUrl}/boxes/${encodeURIComponent(name)}/start`;
        await this._postJson(url, {}, { signal });
        return this.waitForSandbox(name, { timeout, signal });
    }
    /**
     * Stop a running sandbox (preserves sandbox files for later restart).
     *
     * @param name - Sandbox name.
     */
    async stopSandbox(name) {
        const url = `${this._baseUrl}/boxes/${encodeURIComponent(name)}/stop`;
        await this._postJson(url, {});
    }
    // =========================================================================
    // Snapshot Operations
    // =========================================================================
    /**
     * Build a snapshot from a Docker image.
     *
     * Blocks until the snapshot is ready (polls with 2s interval).
     *
     * @param name - Snapshot name.
     * @param dockerImage - Docker image to build from (e.g., "python:3.12-slim").
     * @param fsCapacityBytes - Filesystem capacity in bytes.
     * @param options - Additional options (registry ID, timeout).
     * @returns Snapshot in "ready" status.
     */
    async createSnapshot(name, dockerImage, fsCapacityBytes, options = {}) {
        const { registryId, timeout = 60, signal } = options;
        const url = `${this._baseUrl}/snapshots`;
        const payload = {
            name,
            docker_image: dockerImage,
            fs_capacity_bytes: fsCapacityBytes,
        };
        if (registryId !== undefined) {
            payload.registry_id = registryId;
        }
        const response = await this._postJson(url, payload, { signal });
        const snapshot = (await response.json());
        return this.waitForSnapshot(snapshot.id, { timeout, signal });
    }
    /**
     * Build a snapshot from a local Dockerfile context.
     *
     * Creates a temporary builder sandbox, uploads the Docker build context,
     * runs BuildKit inside the sandbox, and captures the built image as a
     * LangSmith snapshot.
     *
     * @param name - Snapshot name.
     * @param dockerfile - Local Dockerfile path, relative to context by default.
     * @param fsCapacityBytes - Filesystem capacity in bytes.
     * @param options - Build context, args, target, build log callback, builder
     *   vCPUs/memory, timeout.
     * @returns Snapshot in "ready" status.
     */
    async createSnapshotFromDockerfile(name, dockerfile, fsCapacityBytes, options = {}) {
        const { context = ".", buildArgs, target, onBuildLog, vCpus, memBytes, timeout = 60, } = options;
        const { contextPath, dockerfileRel } = await resolveDockerfileContext(dockerfile, context);
        const builderName = `snapshot-builder-${uuidv4().replace(/-/g, "").slice(0, 12)}`;
        // Stage the build on the capacity-backed root filesystem, not /tmp.
        // Inside the sandbox /tmp is a RAM-backed tmpfs that fsCapacityBytes does
        // not size, and BuildKit's native snapshotter writes a full copy of every
        // layer under its root, so a /tmp build exhausts guest RAM and fails with
        // "No space left on device".
        const buildRoot = `/var/lib/langsmith-build/${uuidv4()
            .replace(/-/g, "")
            .slice(0, 12)}`;
        const remoteContext = `${buildRoot}/context`;
        const remoteTar = `${buildRoot}/context.tar`;
        const imageRef = `langsmith-snapshot-build:${uuidv4().replace(/-/g, "")}`;
        const buildkitRoot = `${buildRoot}/buildkit-root`;
        const buildkitRun = `${buildRoot}/buildkit-run`;
        const builder = await this.createSandbox({
            name: builderName,
            timeout,
            vCpus,
            memBytes,
            fsCapacityBytes,
        });
        try {
            await builder.write(remoteTar, await makeDockerContextTar(contextPath), timeout);
            await builder.run([
                `rm -rf ${shellQuote(remoteContext)}`,
                `mkdir -p ${shellQuote(remoteContext)}`,
                `tar -xf ${shellQuote(remoteTar)} -C ${shellQuote(remoteContext)}`,
            ].join(" && "), { timeout });
            const result = await builder.run(makeDockerfileBuildCommand({
                remoteContext,
                dockerfileRel,
                imageRef,
                buildkitRoot,
                buildkitRun,
                buildArgs,
                target,
            }), {
                timeout,
                onStdout: onBuildLog,
                onStderr: onBuildLog,
            });
            if (result.exit_code !== 0) {
                throw new LangSmithResourceCreationError("Dockerfile snapshot build failed", "snapshot");
            }
            return await this.captureSnapshot(builder.name, name, {
                dockerImage: imageRef,
                fsCapacityBytes,
                timeout,
            });
        }
        finally {
            await builder.delete();
        }
    }
    /**
     * Capture a snapshot from a running sandbox.
     *
     * Blocks until the snapshot is ready (polls with 2s interval).
     *
     * @param sandboxName - Name of the sandbox to capture from.
     * @param name - Snapshot name.
     * @param options - Capture options (timeout).
     * @returns Snapshot in "ready" status.
     */
    async captureSnapshot(sandboxName, name, options = {}) {
        const { dockerImage, fsCapacityBytes, timeout = 60, signal } = options;
        const url = `${this._baseUrl}/boxes/${encodeURIComponent(sandboxName)}/snapshot`;
        const payload = { name };
        if (dockerImage !== undefined) {
            payload.docker_image = dockerImage;
        }
        if (fsCapacityBytes !== undefined) {
            payload.fs_capacity_bytes = fsCapacityBytes;
        }
        const response = await this._postJson(url, payload, { signal });
        const snapshot = (await response.json());
        return this.waitForSnapshot(snapshot.id, { timeout, signal });
    }
    /**
     * Get a snapshot by ID.
     *
     * @param snapshotId - Snapshot UUID.
     * @returns Snapshot.
     */
    async getSnapshot(snapshotId, options) {
        const url = `${this._baseUrl}/snapshots/${encodeURIComponent(snapshotId)}`;
        const response = await this._fetch(url, { signal: options?.signal });
        if (!response.ok) {
            if (response.status === 404) {
                throw new LangSmithResourceNotFoundError(`Snapshot '${snapshotId}' not found`, "snapshot");
            }
            await handleClientHttpError(response);
        }
        return (await response.json());
    }
    /**
     * List snapshots, optionally filtered and paginated server-side.
     *
     * The backend always paginates this endpoint. When `limit` is omitted the
     * server applies a default page size (currently 50), so a single call is
     * not guaranteed to return every snapshot visible to the tenant. To iterate
     * through all results, repeat the call with increasing `offset` values (or
     * an explicit `limit`) until fewer than `limit` snapshots come back.
     *
     * @param options - Optional filter/pagination options.
     *   - `nameContains`: case-insensitive substring match on snapshot name.
     *   - `limit`: page size; must be between 1 and 500 (inclusive). Defaults
     *     to 50 server-side when omitted.
     *   - `offset`: number of snapshots to skip; must be `>= 0`.
     *
     *   Values outside those ranges are rejected by the server.
     * @returns A single page of Snapshots matching the provided filters.
     *
     * @example
     * ```typescript
     * const firstPage = await client.listSnapshots();
     * const page = await client.listSnapshots({
     *   nameContains: "python",
     *   limit: 100,
     *   offset: 0,
     * });
     * ```
     */
    async listSnapshots(options = {}) {
        const { nameContains, limit, offset, signal } = options;
        const params = new URLSearchParams();
        if (nameContains !== undefined) {
            params.set("name_contains", nameContains);
        }
        if (limit !== undefined) {
            params.set("limit", String(limit));
        }
        if (offset !== undefined) {
            params.set("offset", String(offset));
        }
        const query = params.toString();
        const url = query
            ? `${this._baseUrl}/snapshots?${query}`
            : `${this._baseUrl}/snapshots`;
        const response = await this._fetch(url, { signal });
        if (!response.ok) {
            await handleClientHttpError(response);
        }
        const data = await response.json();
        return (data.snapshots ?? []);
    }
    /**
     * Delete a snapshot.
     *
     * @param snapshotId - Snapshot UUID.
     */
    async deleteSnapshot(snapshotId) {
        const url = `${this._baseUrl}/snapshots/${encodeURIComponent(snapshotId)}`;
        const response = await this._fetch(url, { method: "DELETE" });
        if (!response.ok) {
            await handleClientHttpError(response);
        }
    }
    /**
     * Poll until a snapshot reaches "ready" or "failed" status.
     *
     * @param snapshotId - Snapshot UUID.
     * @param options - Polling options (timeout, pollInterval).
     * @returns Snapshot in "ready" status.
     */
    async waitForSnapshot(snapshotId, options = {}) {
        const { timeout = 300, pollInterval = 2.0, signal } = options;
        const deadline = Date.now() + timeout * 1000;
        let lastStatus = "building";
        while (Date.now() < deadline) {
            signal?.throwIfAborted();
            const snapshot = await this.getSnapshot(snapshotId, { signal });
            lastStatus = snapshot.status;
            if (snapshot.status === "ready") {
                return snapshot;
            }
            if (snapshot.status === "failed") {
                throw new LangSmithResourceCreationError(snapshot.status_message ?? `Snapshot '${snapshotId}' build failed`, "snapshot");
            }
            // Cap sleep to remaining time + jitter
            const remaining = deadline - Date.now();
            const jitter = pollInterval * 200 * (Math.random() - 0.5); // ±10%
            const delay = Math.min(pollInterval * 1000 + jitter, remaining);
            if (delay > 0) {
                await sleepWithSignal(delay, signal);
            }
        }
        throw new LangSmithResourceTimeoutError(`Snapshot '${snapshotId}' did not become ready within ${timeout}s`, "snapshot", lastStatus);
    }
    /**
     * Returns a string representation of the SandboxClient instance.
     * This method is called when the object is converted to a string
     * or logged, ensuring sensitive information like API keys is not exposed.
     *
     * @returns A string representation of the SandboxClient.
     */
    toString() {
        return `[LangSmithSandboxClient apiEndpoint=${JSON.stringify(this._baseUrl)}]`;
    }
    /**
     * Custom inspect method for Node.js.
     * This method is called when the object is inspected in the Node.js REPL
     * or with console.log, ensuring sensitive information like API keys is not exposed.
     *
     * @returns A string representation of the SandboxClient for inspection.
     */
    [Symbol.for("nodejs.util.inspect.custom")]() {
        return this.toString();
    }
}
