Ctrl K
ComunidadGuías…

scripts/cua-linux-release.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { createHash } from "node:crypto";
import { execFile } from "node:child_process";
import {
  chmod,
  copyFile,
  lstat,
  mkdir,
  mkdtemp,
  readFile,
  readdir,
  realpath,
  rename,
  rm,
  writeFile,
} from "node:fs/promises";
import path from "node:path";
import { promisify } from "node:util";
import { gunzipSync } from "node:zlib";

const run = promisify(execFile);
const KIB = 1024;
const MIB = KIB * KIB;
const TAR_BLOCK_SIZE = 512;
export const LICENSE_FILES = Object.freeze([
  "LICENSE.md",
  "Inter-OFL-1.1.txt",
  "THIRD_PARTY_LICENSES.html",
  "THIRD_PARTY_NOTICES.md",
  "SBOM.cdx.json",
]);

export const LINUX_CUA_RELEASE = Object.freeze({
  version: "0.41.3",
  archiveName: "cua-driver-rs-0.41.3-linux-x86_64-binary.tar.gz",
  archiveSize: 38_375_345,
  archiveSha256: "a00a7bc4a648adbcbc93bfbf5a058daf71ce8838ee172e33b197aaf649ecee14",
  driverSha256: "83030028e7c9e3f4d753b3b27025135d44eb8d52f562cce0aaaf95777bc1b41f",
  cursorThemeSha256: "d39efb960d207e7328d7a9dd48e3b6565f74be073f9c2ca8dcba86a4d0f28312",
  url:
    "https://github.com/trycua/cua/releases/download/cua-driver-rs-v0.41.3/" +
    "cua-driver-rs-0.41.3-linux-x86_64-binary.tar.gz",
  maxArchiveBytes: 64 * MIB,
  maxExpandedBytes: 128 * MIB,
});

// Pin the complete upstream archive shape, even though OpenMausBot extracts
// only the two CLI runtime files. A release that silently grows a new native
// payload must receive an explicit review and checksum update first.
const ARCHIVE_MEMBERS = Object.freeze({
  "cua-driver": Object.freeze({
    kind: "file",
    size: 61_925_800,
    sha256: LINUX_CUA_RELEASE.driverSha256,
    staged: true,
    mode: 0o755,
  }),
  "cua-cursor-theme": Object.freeze({
    kind: "file",
    size: 5_090_720,
    sha256: LINUX_CUA_RELEASE.cursorThemeSha256,
    staged: true,
    mode: 0o755,
  }),
  "libcua_driver_sdk.so": Object.freeze({
    kind: "file",
    size: 51_420_328,
    sha256: "a6402f2e892089cdece4a622b062be734683f771c8d7b4a0b122895974673685",
  }),
  "cua_driver_node_runtime.node": Object.freeze({
    kind: "file",
    size: 972_456,
    sha256: "4441cf353e15773db90707543c3a4cab64f3312f001de825f117c5df95f8b375",
  }),
  "cua_driver_abi.h": Object.freeze({
    kind: "file",
    size: 7_762,
    sha256: "e952620e41ac81b2d900886c7b0a24ebc6268a4edb8df88cc9971ae34af4ba0d",
  }),
  "wayland-helper/": Object.freeze({ kind: "directory", size: 0 }),
  "wayland-helper/winrects@cua/": Object.freeze({ kind: "directory", size: 0 }),
  "wayland-helper/winrects@cua/extension.js": Object.freeze({
    kind: "file",
    size: 47_360,
    sha256: "b7b5b922e68a8ee53eb549a3d2f85dd28259140633c0c34123f16910f7ac8e0c",
  }),
  "wayland-helper/winrects@cua/metadata.json": Object.freeze({
    kind: "file",
    size: 231,
    sha256: "1853170d66280f1a096af0e33bb400e3740cddf07bf3e2f1e3bbeaf33550e1d0",
  }),
  "wayland-helper/install.sh": Object.freeze({
    kind: "file",
    size: 1_932,
    sha256: "1f36c163562d8d7a3a711e69625adc33895a511a1ca0f2bb8f516c3aca0906fb",
  }),
  "wayland-helper/README.md": Object.freeze({
    kind: "file",
    size: 8_974,
    sha256: "359bc52eb9639b7242a05d597e4a2ac6d0ea91622d4d4eed89412b594d5f2e17",
  }),
});

export function sha256(bytes) {
  return createHash("sha256").update(bytes).digest("hex");
}

function tarString(buffer, start, length) {
  const field = buffer.subarray(start, start + length);
  const nul = field.indexOf(0);
  return field.subarray(0, nul === -1 ? field.length : nul).toString("utf8");
}

function tarOctal(buffer, start, length, label) {
  const raw = tarString(buffer, start, length).trim();
  if (!raw) return 0;
  if (!/^[0-7]+$/.test(raw)) throw new Error(`invalid tar ${label}`);
  const value = Number.parseInt(raw, 8);
  if (!Number.isSafeInteger(value) || value < 0) throw new Error(`invalid tar ${label}`);
  return value;
}

function tarHeaderChecksum(header) {
  const copy = Buffer.from(header);
  copy.fill(0x20, 148, 156);
  return copy.reduce((sum, value) => sum + value, 0);
}

function safeArchivePath(name) {
  if (
    !name ||
    name.includes("\\") ||
    name.includes("\0") ||
    path.posix.isAbsolute(name) ||
    name.split("/").some((component) => component === ".." || component === ".")
  ) {
    throw new Error(`unsafe CUA archive path: ${JSON.stringify(name)}`);
  }
  return name;
}

/**
 * Parse the pinned tarball without delegating extraction to a general-purpose
 * archive tool. Only regular files and directories from the reviewed manifest
 * are accepted; links, PAX/GNU extensions, devices and duplicate names fail.
 */
export function parseLinuxCuaArchive(
  archiveBytes,
  release = LINUX_CUA_RELEASE,
  archiveMembers = ARCHIVE_MEMBERS,
) {
  if (!Buffer.isBuffer(archiveBytes)) archiveBytes = Buffer.from(archiveBytes);
  if (archiveBytes.length > release.maxArchiveBytes) {
    throw new Error("CUA archive exceeds the compressed size limit");
  }
  if (release.archiveSize != null && archiveBytes.length !== release.archiveSize) {
    throw new Error("CUA archive size does not match the pinned release");
  }
  if (sha256(archiveBytes) !== release.archiveSha256) {
    throw new Error("CUA archive checksum mismatch");
  }

  let tar;
  try {
    tar = gunzipSync(archiveBytes, { maxOutputLength: release.maxExpandedBytes });
  } catch (error) {
    throw new Error(`CUA archive could not be decompressed safely: ${error?.message ?? error}`);
  }
  if (tar.length > release.maxExpandedBytes) {
    throw new Error("CUA archive exceeds the expanded size limit");
  }

  const seen = new Set();
  const staged = new Map();
  let offset = 0;
  let ended = false;
  while (offset + TAR_BLOCK_SIZE <= tar.length) {
    const header = tar.subarray(offset, offset + TAR_BLOCK_SIZE);
    if (header.every((value) => value === 0)) {
      ended = true;
      break;
    }

    const expectedChecksum = tarOctal(header, 148, 8, "checksum");
    if (expectedChecksum !== tarHeaderChecksum(header)) {
      throw new Error("CUA archive contains an invalid tar header checksum");
    }
    const prefix = tarString(header, 345, 155);
    const basename = tarString(header, 0, 100);
    const name = safeArchivePath(prefix ? `${prefix}/${basename}` : basename);
    if (seen.has(name)) throw new Error(`duplicate CUA archive member: ${name}`);
    seen.add(name);

    const expected = archiveMembers[name];
    if (!expected) throw new Error(`unexpected CUA archive member: ${name}`);
    const typeByte = header[156];
    const kind = typeByte === 0 || typeByte === 0x30 ? "file" : typeByte === 0x35 ? "directory" : "special";
    if (kind !== expected.kind) {
      throw new Error(`unsupported CUA archive member type for ${name}`);
    }
    if (tarString(header, 157, 100)) {
      throw new Error(`CUA archive links are not allowed: ${name}`);
    }

    const size = tarOctal(header, 124, 12, "member size");
    if (size !== expected.size) throw new Error(`unexpected CUA archive member size for ${name}`);
    const dataOffset = offset + TAR_BLOCK_SIZE;
    const paddedSize = Math.ceil(size / TAR_BLOCK_SIZE) * TAR_BLOCK_SIZE;
    const nextOffset = dataOffset + paddedSize;
    if (nextOffset > tar.length) throw new Error(`truncated CUA archive member: ${name}`);

    if (kind === "file") {
      const bytes = Buffer.from(tar.subarray(dataOffset, dataOffset + size));
      if (sha256(bytes) !== expected.sha256) {
        throw new Error(`CUA archive member checksum mismatch: ${name}`);
      }
      if (expected.staged) staged.set(name, bytes);
    }
    offset = nextOffset;
  }

  if (!ended || !tar.subarray(offset).every((value) => value === 0)) {
    throw new Error("CUA archive has invalid trailing data");
  }
  for (const name of Object.keys(archiveMembers)) {
    if (!seen.has(name)) throw new Error(`CUA archive is missing reviewed member: ${name}`);
  }
  return staged;
}

function releaseManifest() {
  return Object.freeze({
    schemaVersion: 1,
    version: LINUX_CUA_RELEASE.version,
    platform: "linux",
    arch: "x64",
    archive: {
      name: LINUX_CUA_RELEASE.archiveName,
      url: LINUX_CUA_RELEASE.url,
      size: LINUX_CUA_RELEASE.archiveSize,
      sha256: LINUX_CUA_RELEASE.archiveSha256,
    },
    sourceCommit: "bd0e299c67dec6397b3d41a259452b2ea13a1cd5",
    files: {
      "cua-driver": {
        sha256: LINUX_CUA_RELEASE.driverSha256,
        mode: "0755",
      },
      "cua-cursor-theme": {
        sha256: LINUX_CUA_RELEASE.cursorThemeSha256,
        mode: "0755",
      },
    },
  });
}

async function binaryVersion(binary) {
  const { stdout } = await run(binary, ["--version"], {
    encoding: "utf8",
    timeout: 5_000,
    maxBuffer: 256 * KIB,
    env: {
      LANG: "C",
      LC_ALL: "C",
      CUA_DRIVER_RS_UPDATE_CHECK: "false",
      CUA_DRIVER_RS_TELEMETRY_ENABLED: "false",
    },
  });
  return stdout.trim();
}

async function validateBinaryManifest(binary) {
  const { stdout } = await run(binary, ["manifest"], {
    encoding: "utf8",
    timeout: 5_000,
    maxBuffer: 256 * KIB,
    env: {
      LANG: "C",
      LC_ALL: "C",
      CUA_DRIVER_RS_UPDATE_CHECK: "false",
      CUA_DRIVER_RS_TELEMETRY_ENABLED: "false",
    },
  });
  const manifest = JSON.parse(stdout);
  const invocationCommand = manifest.mcp_invocation?.command;
  const invocationPath =
    typeof invocationCommand === "string" && invocationCommand.length > 0
      ? await realpath(invocationCommand).catch(() => null)
      : null;
  if (
    manifest.schema_version !== "1" ||
    manifest.binary_version !== LINUX_CUA_RELEASE.version ||
    invocationPath !== (await realpath(binary)) ||
    JSON.stringify(manifest.mcp_invocation?.args) !== JSON.stringify(["mcp"])
  ) {
    throw new Error("staged CUA Driver returned an incompatible manifest");
  }
}

export async function validateStagedLayout(stageDirectory) {
  const stageDetails = await lstat(stageDirectory);
  if (
    !stageDetails.isDirectory() ||
    stageDetails.isSymbolicLink() ||
    (stageDetails.mode & 0o777) !== 0o755
  ) {
    throw new Error("staged CUA release root must be a real 0755 directory");
  }
  const rootEntries = (await readdir(stageDirectory)).sort();
  const expectedRootEntries = ["cua-cursor-theme", "cua-driver", "licenses", "release.json"];
  if (JSON.stringify(rootEntries) !== JSON.stringify(expectedRootEntries)) {
    throw new Error("staged CUA release contains unexpected or missing entries");
  }
  const licensesDirectory = path.join(stageDirectory, "licenses");
  const licenseDirectoryDetails = await lstat(licensesDirectory);
  if (
    !licenseDirectoryDetails.isDirectory() ||
    licenseDirectoryDetails.isSymbolicLink() ||
    (licenseDirectoryDetails.mode & 0o777) !== 0o755
  ) {
    throw new Error("staged CUA licenses must be a real 0755 directory");
  }
  const licenseEntries = (await readdir(licensesDirectory)).sort();
  if (JSON.stringify(licenseEntries) !== JSON.stringify([...LICENSE_FILES].sort())) {
    throw new Error("staged CUA licenses contain unexpected or missing entries");
  }
  return { licensesDirectory };
}

async function validateStagedRuntime(stageDirectory, { licenseDirectory }) {
  const { licensesDirectory } = await validateStagedLayout(stageDirectory);
  const manifestDetails = await lstat(path.join(stageDirectory, "release.json"));
  if (
    !manifestDetails.isFile() ||
    manifestDetails.isSymbolicLink() ||
    (manifestDetails.mode & 0o777) !== 0o644
  ) {
    throw new Error("staged CUA release manifest must be a regular 0644 file");
  }
  const manifest = JSON.parse(
    await readFile(path.join(stageDirectory, "release.json"), "utf8"),
  );
  if (JSON.stringify(manifest) !== JSON.stringify(releaseManifest())) {
    throw new Error("staged CUA release manifest does not match the pinned release");
  }
  for (const [name, expected] of Object.entries(releaseManifest().files)) {
    const file = path.join(stageDirectory, name);
    const details = await lstat(file);
    if (
      !details.isFile() ||
      details.isSymbolicLink() ||
      (details.mode & 0o777) !== Number.parseInt(expected.mode, 8)
    ) {
      throw new Error(`staged CUA runtime has an invalid mode: ${name}`);
    }
    if (sha256(await readFile(file)) !== expected.sha256) {
      throw new Error(`staged CUA runtime checksum mismatch: ${name}`);
    }
  }
  for (const name of LICENSE_FILES) {
    const details = await lstat(path.join(licensesDirectory, name));
    if (!details.isFile() || details.isSymbolicLink() || (details.mode & 0o777) !== 0o644) {
      throw new Error(`staged CUA license resource has an invalid type or mode: ${name}`);
    }
    const expected = await readFile(path.join(licenseDirectory, name));
    const actual = await readFile(path.join(licensesDirectory, name));
    if (!actual.equals(expected)) throw new Error(`staged CUA license resource mismatch: ${name}`);
  }
  if ((await binaryVersion(path.join(stageDirectory, "cua-driver"))) !== `cua-driver ${LINUX_CUA_RELEASE.version}`) {
    throw new Error(`staged CUA Driver does not report version ${LINUX_CUA_RELEASE.version}`);
  }
  await validateBinaryManifest(path.join(stageDirectory, "cua-driver"));
  return manifest;
}

export async function readBoundedResponseBody(response, maxBytes) {
  const declaredLength = Number(response.headers?.get?.("content-length"));
  if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
    await response.body?.cancel?.();
    throw new Error("downloaded CUA archive exceeds the compressed size limit");
  }
  if (!response.body) throw new Error("CUA Driver download returned no response body");
  const chunks = [];
  let total = 0;
  try {
    for await (const chunk of response.body) {
      const bytes = Buffer.from(chunk);
      total += bytes.length;
      if (total > maxBytes) {
        throw new Error("downloaded CUA archive exceeds the compressed size limit");
      }
      chunks.push(bytes);
    }
  } catch (error) {
    try {
      await response.body.cancel?.();
    } catch {}
    throw error;
  }
  return Buffer.concat(chunks, total);
}

async function readVerifiedArchive({
  archivePath,
  cachePath,
  fetchImpl,
  offline,
  downloadTimeoutMs,
}) {
  for (const candidate of [archivePath, cachePath].filter(Boolean)) {
    try {
      const bytes = await readFile(candidate);
      if (
        bytes.length === LINUX_CUA_RELEASE.archiveSize &&
        sha256(bytes) === LINUX_CUA_RELEASE.archiveSha256
      ) {
        return bytes;
      }
      if (candidate === archivePath) throw new Error("provided CUA archive checksum mismatch");
    } catch (error) {
      if (candidate === archivePath || error?.code !== "ENOENT") throw error;
    }
  }
  if (offline) throw new Error("offline CUA staging requires an existing verified stage or archive cache");

  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), downloadTimeoutMs);
  timeout.unref?.();
  let bytes;
  try {
    const response = await fetchImpl(LINUX_CUA_RELEASE.url, {
      headers: { "user-agent": "OpenMausBot-packager" },
      signal: controller.signal,
    });
    if (!response?.ok) {
      throw new Error(`CUA Driver download failed: HTTP ${response?.status ?? "unknown"}`);
    }
    bytes = await readBoundedResponseBody(response, LINUX_CUA_RELEASE.maxArchiveBytes);
  } catch (error) {
    if (controller.signal.aborted) throw new Error("CUA Driver download timed out");
    throw error;
  } finally {
    clearTimeout(timeout);
  }
  if (bytes.length !== LINUX_CUA_RELEASE.archiveSize) {
    throw new Error("downloaded CUA archive size does not match the pinned release");
  }
  if (sha256(bytes) !== LINUX_CUA_RELEASE.archiveSha256) {
    throw new Error("downloaded CUA archive checksum mismatch");
  }
  const cacheDirectory = path.dirname(cachePath);
  await mkdir(cacheDirectory, { recursive: true });
  const temporaryDirectory = await mkdtemp(path.join(cacheDirectory, ".cua-archive-"));
  try {
    const temporaryArchive = path.join(temporaryDirectory, LINUX_CUA_RELEASE.archiveName);
    await writeFile(temporaryArchive, bytes, { mode: 0o600, flag: "wx" });
    await rename(temporaryArchive, cachePath);
  } finally {
    await rm(temporaryDirectory, { recursive: true, force: true });
  }
  return bytes;
}

export async function stageLinuxCua({
  rootDirectory,
  platform = process.platform,
  arch = process.arch,
  offline = false,
  archivePath,
  fetchImpl = globalThis.fetch,
  downloadTimeoutMs = 120_000,
} = {}) {
  if (platform !== "linux" || arch !== "x64") {
    throw new Error(`bundled CUA staging supports only linux/x64, received ${platform}/${arch}`);
  }
  if (!rootDirectory || !path.isAbsolute(rootDirectory)) {
    throw new Error("rootDirectory must be an absolute path");
  }
  // Linux local control spawns this reviewed CLI directly and deliberately
  // excludes the npm SDK's native .node/.so files. Do not couple the Linux
  // release pin to @trycua/cua-driver, which serves the separate macOS SDK
  // path and may advance independently.

  const stageDirectory = path.join(rootDirectory, "dist-native", "cua-linux-x64");
  const licenseDirectory = path.join(rootDirectory, "third_party", "cua-driver");
  if (offline) {
    try {
      await validateStagedRuntime(stageDirectory, { licenseDirectory });
      return { stageDirectory, source: "existing-stage", manifest: releaseManifest() };
    } catch {
      // An invalid stage is never trusted. A verified cache may still satisfy
      // an explicitly offline release build below.
    }
  }

  const cachePath = path.join(
    rootDirectory,
    "node_modules",
    ".cache",
    "openmausbot",
    LINUX_CUA_RELEASE.archiveName,
  );
  const archive = await readVerifiedArchive({
    archivePath,
    cachePath,
    fetchImpl,
    offline,
    downloadTimeoutMs,
  });
  const files = parseLinuxCuaArchive(archive);

  const stageParent = path.dirname(stageDirectory);
  await mkdir(stageParent, { recursive: true });
  const temporary = await mkdtemp(path.join(stageParent, ".cua-linux-x64-"));
  const backup = `${stageDirectory}.previous`;
  let previousMoved = false;
  try {
    await chmod(temporary, 0o755);
    for (const [name, bytes] of files) {
      const expected = ARCHIVE_MEMBERS[name];
      await writeFile(path.join(temporary, name), bytes, { mode: expected.mode, flag: "wx" });
      await chmod(path.join(temporary, name), expected.mode);
    }
    await mkdir(path.join(temporary, "licenses"), { mode: 0o755 });
    await chmod(path.join(temporary, "licenses"), 0o755);
    for (const name of LICENSE_FILES) {
      await copyFile(path.join(licenseDirectory, name), path.join(temporary, "licenses", name));
      await chmod(path.join(temporary, "licenses", name), 0o644);
    }
    await writeFile(path.join(temporary, "release.json"), `${JSON.stringify(releaseManifest(), null, 2)}\n`, {
      mode: 0o644,
      flag: "wx",
    });
    await validateStagedRuntime(temporary, { licenseDirectory });

    await rm(backup, { recursive: true, force: true });
    try {
      await rename(stageDirectory, backup);
      previousMoved = true;
    } catch (error) {
      if (error?.code !== "ENOENT") throw error;
    }
    await rename(temporary, stageDirectory);
    if (previousMoved) await rm(backup, { recursive: true, force: true });
  } catch (error) {
    await rm(temporary, { recursive: true, force: true });
    if (previousMoved) {
      try {
        await rename(backup, stageDirectory);
      } catch {}
    }
    throw error;
  }

  return { stageDirectory, source: archivePath ? "archive" : "official-release", manifest: releaseManifest() };
}