Ctrl K
ComunidadGuías…

src/domain/assistant.js

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

Archivo original ↗Preguntar a KiBot
import {
  INSTALL_ROOT,
  MANAGED_MARKER_PATH,
  SHARED_ROOT_COMMAND,
  SHARED_ROOT_MARKER_CONTENT,
  SHARED_ROOT_MARKER_PATH,
  SIDECAR_MARKER_CONTENT,
} from "./safety.js";
import { validateDockerName } from "./validation.js";

export const ASSISTANT_ROOT = INSTALL_ROOT + "/assistant-sandbox";
export const ASSISTANT_MARKER_PATH =
  ASSISTANT_ROOT + "/.managed-by-relmio-ai-assistant";
export { SHARED_ROOT_MARKER_PATH as ASSISTANT_ROOT_MARKER_PATH };

const ASSISTANT_MARKER_VERSION = 2;
const LEGACY_ASSISTANT_MARKER_VERSION = 1;
const INSTALL_ID_PATTERN = /^[a-f0-9]{32}$/u;
const PROJECT_NAME_PATTERN = /^relmio-ai-[a-f0-9]{32}$/u;
const SANDBOX_ALIAS_PATTERN = /^relmio-ai-sandbox-[a-f0-9]{32}$/u;
const SEARXNG_ALIAS_PATTERN = /^relmio-ai-searxng-[a-f0-9]{32}$/u;
const MAX_MARKER_BYTES = 512;

const MANAGED_LABEL = "io.relmio.ai-assistant.managed";
const INSTALL_ID_LABEL = "io.relmio.ai-assistant.install-id";
const EXPECTED_MANAGED_LABEL_VALUE = "true";
const SANDBOX_SERVICE_NAMES = Object.freeze([
  "relmio-sandbox-certs",
  "relmio-sandbox-api",
  "relmio-sandbox-runner-1",
]);
const SEARXNG_SERVICE_NAME = "relmio-searxng";

const ROOT_PRECHECK =
  "if [ -e " + INSTALL_ROOT + " ]; then " +
  "if [ -L " + INSTALL_ROOT + " ] || [ ! -d " + INSTALL_ROOT + " ] || " +
  "[ -L " + SHARED_ROOT_MARKER_PATH + " ] || [ -L " + MANAGED_MARKER_PATH + " ]; then exit 43; " +
  "elif [ -e " + SHARED_ROOT_MARKER_PATH + " ]; then " +
  "if [ ! -f " + SHARED_ROOT_MARKER_PATH + " ] || [ \"$(cat " + SHARED_ROOT_MARKER_PATH + ")\" != \"" +
  SHARED_ROOT_MARKER_CONTENT.trim() + "\" ]; then exit 42; fi; " +
  "elif [ -f " + MANAGED_MARKER_PATH + " ] && [ \"$(cat " +
  MANAGED_MARKER_PATH + ")\" = \"" + SIDECAR_MARKER_CONTENT.trim() +
  "\" ]; then :; else exit 42; fi; fi";

export const ASSISTANT_PRECHECK_COMMAND =
  ROOT_PRECHECK + "; if [ -L " + ASSISTANT_ROOT + " ]; then exit 43; " +
  "elif [ -e " + ASSISTANT_ROOT + " ]; then " +
  "if [ ! -d " + ASSISTANT_ROOT + " ]; then exit 42; " +
  "elif [ -L " + ASSISTANT_MARKER_PATH + " ] || [ -L " +
  ASSISTANT_ROOT + "/docker-compose.yml ] || [ -L " +
  ASSISTANT_ROOT + "/searxng-settings.yml ] || [ -L " +
  ASSISTANT_ROOT + "/.env ]; then exit 43; " +
  "elif [ ! -f " + ASSISTANT_MARKER_PATH + " ]; then exit 42; " +
  "else printf '%s\\n' managed; cat " + ASSISTANT_MARKER_PATH +
  "; fi; else printf '%s\\n' new; fi";

function createInstallHex(randomBytes) {
  const value = randomBytes(16);
  if (!Buffer.isBuffer(value) || value.length !== 16) {
    throw new TypeError("A cryptographic AI Assistant install generator is required.");
  }
  return value.toString("hex");
}

function objectHasExactKeys(value, keys) {
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
  const actual = Object.keys(value).sort();
  const expected = [...keys].sort();
  return actual.length === expected.length &&
    actual.every((key, index) => key === expected[index]);
}

export function validateAssistantInstallation(value) {
  const legacyKeys = [
    "version",
    "installId",
    "projectName",
    "sandboxAlias",
    "searxngAlias",
  ];
  if (
    value?.version === LEGACY_ASSISTANT_MARKER_VERSION &&
    objectHasExactKeys(value, legacyKeys)
  ) {
    return validateAssistantInstallation({
      ...value,
      version: ASSISTANT_MARKER_VERSION,
      includeSearxng: true,
    });
  }
  const requiredKeys = [...legacyKeys, "includeSearxng"];
  if (
    !objectHasExactKeys(value, requiredKeys) ||
    value.version !== ASSISTANT_MARKER_VERSION ||
    typeof value.installId !== "string" ||
    !INSTALL_ID_PATTERN.test(value.installId) ||
    typeof value.projectName !== "string" ||
    !PROJECT_NAME_PATTERN.test(value.projectName) ||
    typeof value.sandboxAlias !== "string" ||
    !SANDBOX_ALIAS_PATTERN.test(value.sandboxAlias) ||
    typeof value.searxngAlias !== "string" ||
    !SEARXNG_ALIAS_PATTERN.test(value.searxngAlias) ||
    typeof value.includeSearxng !== "boolean" ||
    value.sandboxAlias === value.searxngAlias ||
    value.sandboxAlias.endsWith(value.installId) ||
    value.searxngAlias.endsWith(value.installId) ||
    value.sandboxAlias.slice(-32) === value.searxngAlias.slice(-32)
  ) {
    throw new TypeError("The AI Assistant installation marker is invalid.");
  }
  return {
    version: ASSISTANT_MARKER_VERSION,
    installId: value.installId,
    projectName: value.projectName,
    sandboxAlias: value.sandboxAlias,
    searxngAlias: value.searxngAlias,
    includeSearxng: value.includeSearxng,
  };
}

export function validateAssistantSearxngSelection(includeSearxng) {
  if (typeof includeSearxng !== "boolean") {
    throw new TypeError("Choose whether to include optional SearXNG web search.");
  }
  return includeSearxng;
}

export function createAssistantInstallation({ randomBytes, includeSearxng }) {
  if (typeof randomBytes !== "function") {
    throw new TypeError("A cryptographic AI Assistant install generator is required.");
  }
  const selectedSearxng = validateAssistantSearxngSelection(includeSearxng);
  const installId = createInstallHex(randomBytes);
  const projectSuffix = createInstallHex(randomBytes);
  const sandboxSuffix = createInstallHex(randomBytes);
  const searxngSuffix = createInstallHex(randomBytes);
  return validateAssistantInstallation({
    version: ASSISTANT_MARKER_VERSION,
    installId,
    projectName: "relmio-ai-" + projectSuffix,
    sandboxAlias: "relmio-ai-sandbox-" + sandboxSuffix,
    searxngAlias: "relmio-ai-searxng-" + searxngSuffix,
    includeSearxng: selectedSearxng,
  });
}

export function serializeAssistantMarker(installation) {
  return JSON.stringify(validateAssistantInstallation(installation)) + "\n";
}

export function parseAssistantMarker(contents) {
  if (
    typeof contents !== "string" ||
    contents.length === 0 ||
    Buffer.byteLength(contents, "utf8") > MAX_MARKER_BYTES
  ) {
    throw new TypeError("The AI Assistant installation marker is invalid.");
  }
  try {
    return validateAssistantInstallation(JSON.parse(contents));
  } catch {
    throw new TypeError("The AI Assistant installation marker is invalid.");
  }
}

export function parseAssistantPrecheck(output) {
  if (typeof output !== "string" || output.length === 0) {
    throw new TypeError("The VPS AI Assistant install-directory check returned an invalid state.");
  }
  if (output === "new\n") return { state: "new", installation: null };
  if (!output.startsWith("managed\n")) {
    throw new TypeError("The VPS AI Assistant install-directory check returned an invalid state.");
  }
  return {
    state: "managed",
    installation: parseAssistantMarker(output.slice("managed\n".length)),
  };
}

function composePrefix(installation) {
  const safeInstallation = validateAssistantInstallation(installation);
  return "docker compose --project-name " + safeInstallation.projectName +
    " --file " + ASSISTANT_ROOT + "/docker-compose.yml";
}

export function getAssistantServiceNames(installation) {
  const safeInstallation = validateAssistantInstallation(installation);
  return safeInstallation.includeSearxng
    ? [...SANDBOX_SERVICE_NAMES, SEARXNG_SERVICE_NAME]
    : [...SANDBOX_SERVICE_NAMES];
}

export function getAssistantContainerNames(installation) {
  const safeInstallation = validateAssistantInstallation(installation);
  const prefix = "relmio-ai-" + safeInstallation.installId.slice(0, 16) + "-";
  return {
    certs: prefix + "certs",
    api: prefix + "api",
    runner: prefix + "runner",
    ...(safeInstallation.includeSearxng ? { searxng: prefix + "search" } : {}),
  };
}

function validateAssistantStartupScope(installation, startupScope) {
  if (startupScope === "all") return getAssistantServiceNames(installation);
  if (startupScope === "searxng" && installation.includeSearxng) {
    return [SEARXNG_SERVICE_NAME];
  }
  throw new TypeError("The AI Assistant startup scope is invalid.");
}

export function createAssistantDeploymentCommands({ installation, startupScope = "all" }) {
  const safeInstallation = validateAssistantInstallation(installation);
  const prefix = composePrefix(safeInstallation);
  const publicPaths = [ASSISTANT_ROOT + "/docker-compose.yml"];
  if (safeInstallation.includeSearxng) {
    publicPaths.push(ASSISTANT_ROOT + "/searxng-settings.yml");
  }
  const startupServices = validateAssistantStartupScope(safeInstallation, startupScope);
  return [
    // The guard every installer runs first: /docker and the shared folder are root's own, and the
    // shared folder is created or set to mode 0700.
    SHARED_ROOT_COMMAND,
    // Creates the Assistant folder for root only, then proves it is root's 0700 folder and not a link.
    "[ ! -L " + ASSISTANT_ROOT + " ] && if [ ! -e " + ASSISTANT_ROOT + " ]; then mkdir -m 0700 " +
      ASSISTANT_ROOT + "; fi && [ -d " + ASSISTANT_ROOT + " ] && [ \"$(stat -c %u:%a " +
      ASSISTANT_ROOT + ")\" = 0:700 ]",
    "chmod 600 " + SHARED_ROOT_MARKER_PATH + " " + ASSISTANT_MARKER_PATH +
      " " + ASSISTANT_ROOT + "/.env",
    "chmod 644 " + publicPaths.join(" "),
    "test \"$(stat -c '%a' " + SHARED_ROOT_MARKER_PATH + ")\" = 600 && " +
      "test \"$(stat -c '%a' " + ASSISTANT_MARKER_PATH + ")\" = 600 && " +
      "test \"$(stat -c '%a' " + ASSISTANT_ROOT + "/.env)\" = 600",
    publicPaths.map(
      (path) => "test \"$(stat -c '%a' " + path + ")\" = 644",
    ).join(" && "),
    prefix + " config --quiet",
    prefix + " up -d --wait --wait-timeout 90 " + startupServices.join(" "),
  ];
}

export function createAssistantVerificationCommands({ installation, cleanupScope = "project" }) {
  const safeInstallation = validateAssistantInstallation(installation);
  const services = getAssistantServiceNames(safeInstallation).filter(
    (service) => service !== "relmio-sandbox-certs",
  );
  if (cleanupScope !== "project" && cleanupScope !== "searxng") {
    throw new TypeError("The AI Assistant cleanup scope is invalid.");
  }
  if (cleanupScope === "searxng" && !safeInstallation.includeSearxng) {
    throw new TypeError("The AI Assistant cleanup scope is invalid.");
  }
  const prefix = composePrefix(safeInstallation);
  return {
    health: prefix +
      " exec -T relmio-sandbox-api wget -qO- http://127.0.0.1:8080/healthz",
    runningServices: prefix + " ps --status running --services",
    publicationState: prefix + " ps --format json " + services.join(" "),
    cleanup: cleanupScope === "searxng"
      ? prefix + " rm -sf " + SEARXNG_SERVICE_NAME
      : prefix + " down --volumes",
  };
}

export function getAssistantManagedResourceNames(installation) {
  const safeInstallation = validateAssistantInstallation(installation);
  return {
    containers: Object.values(getAssistantContainerNames(safeInstallation)),
    network: safeInstallation.projectName + "-internal",
    volume: safeInstallation.projectName + "-sandbox-tls",
  };
}

function createAttestationCommand(resource, installation) {
  const safeInstallation = validateAssistantInstallation(installation);
  const filter = "--filter \"label=com.docker.compose.project=" +
    safeInstallation.projectName + "\"";
  if (resource === "containers") {
    return "docker container ls -a " + filter +
      " --format '{{.Label \"" + INSTALL_ID_LABEL + "\"}}|{{.Label \"" +
      MANAGED_LABEL + "\"}}'";
  }
  if (resource === "networks") {
    return "docker network ls " + filter +
      " --format '{{.Label \"" + INSTALL_ID_LABEL + "\"}}|{{.Label \"" +
      MANAGED_LABEL + "\"}}'";
  }
  return "docker volume ls " + filter +
    " --format '{{.Label \"" + INSTALL_ID_LABEL + "\"}}|{{.Label \"" +
    MANAGED_LABEL + "\"}}'";
}

export function createAssistantOwnershipAttestationCommands({ installation }) {
  return {
    containers: createAttestationCommand("containers", installation),
    networks: createAttestationCommand("networks", installation),
    volumes: createAttestationCommand("volumes", installation),
  };
}

function createExactResourceCommand(kind, names) {
  const nameField = kind === "containers" ? ".Names" : ".Name";
  const format = " --format '{{" + nameField + "}}|{{.Label \"" + INSTALL_ID_LABEL +
    "\"}}|{{.Label \"" + MANAGED_LABEL + "\"}}'";
  if (kind === "containers") {
    return names.containers.map(
      (name) => "docker container ls -a --filter \"name=" + name + "\"" + format,
    ).join(" && ");
  }
  if (kind === "network") {
    return "docker network ls --filter \"name=" + names.network + "\"" + format;
  }
  return "docker volume ls --filter \"name=" + names.volume + "\"" + format;
}

export function createAssistantExactResourceAttestationCommands({ installation }) {
  const names = getAssistantManagedResourceNames(installation);
  return {
    containers: createExactResourceCommand("containers", names),
    network: createExactResourceCommand("network", names),
    volume: createExactResourceCommand("volume", names),
  };
}

export function createAssistantNetworkCollisionCommand({ networkName, installation }) {
  const safeNetworkName = validateDockerName(networkName);
  validateAssistantInstallation(installation);
  return "ids=$(docker network inspect " + safeNetworkName +
    " --format '{{range $id, $_ := .Containers}}{{$id}}{{\"\\n\"}}{{end}}') && " +
    "if [ -n \"$ids\" ]; then printf '%s\\n' \"$ids\" | xargs -r docker inspect --format '{{.Id}}|{{index .Config.Labels \"" +
    INSTALL_ID_LABEL + "\"}}|{{with index .NetworkSettings.Networks \"" +
    safeNetworkName + "\"}}{{join .Aliases \",\"}}{{end}}'; fi";
}

export function getAssistantLabels(installation) {
  const safeInstallation = validateAssistantInstallation(installation);
  return {
    [MANAGED_LABEL]: EXPECTED_MANAGED_LABEL_VALUE,
    [INSTALL_ID_LABEL]: safeInstallation.installId,
  };
}

export function assertAssistantOnlyCommands({
  commands,
  installation,
  networkName,
  startupScope = "all",
  cleanupScope = "project",
}) {
  if (!Array.isArray(commands) || commands.length === 0) {
    throw new TypeError("An AI Assistant command list is required.");
  }
  const allowed = new Set([
    ASSISTANT_PRECHECK_COMMAND,
    createAssistantNetworkCollisionCommand({ networkName, installation }),
    ...Object.values(createAssistantOwnershipAttestationCommands({ installation })),
    ...Object.values(createAssistantExactResourceAttestationCommands({ installation })),
    ...createAssistantDeploymentCommands({ installation, startupScope }),
    ...Object.values(createAssistantVerificationCommands({ installation, cleanupScope })),
  ]);
  for (const command of commands) {
    if (typeof command !== "string" || !allowed.has(command)) {
      throw new Error(
        "Command rejected: only the managed AI Assistant companion may be changed; n8n must remain untouched.",
      );
    }
  }
  return commands;
}