Ctrl K
ComunidadGuías…

electron/approval-trusted-mode.cjs

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

Archivo original ↗Preguntar a KiBot
"use strict";

const { englishText } = require("./native-text.cjs");
const ERROR_KEYS = Object.freeze({
  "serverError.approval.allThreads": "native.approval.allThreads",
  "serverError.approval.pending": "native.approval.pending",
  "serverError.approval.provider": "native.approval.provider",
});

const BOT_ID = /^[A-Za-z0-9_-]{1,120}$/;
const REQUEST_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
const APPROVAL_MODES = new Set(["ask", "edits", "auto", "full", "custom"]);

function plainObject(value) {
  return value && typeof value === "object" && !Array.isArray(value);
}

function trustedApprovalModeRequest(requestId, botId, mode, acknowledgeLocalAuto = false, threadId, modelSelection, updateBotDefault, threadOnly = false, allThreads = false, refreshPermissions = false) {
  if (typeof requestId !== "string" || !REQUEST_ID.test(requestId)) {
    throw new Error("invalid trusted approval-mode request id");
  }
  if (typeof botId !== "string" || !BOT_ID.test(botId)) {
    throw new Error("invalid bot id for trusted approval mode");
  }
  if (!APPROVAL_MODES.has(mode)) throw new Error("invalid trusted approval mode");
  if (typeof allThreads !== "boolean" || (allThreads && (threadId !== undefined || threadOnly ||
    modelSelection !== undefined || updateBotDefault !== undefined || mode === "custom"))) {
    throw new Error("invalid all-threads approval selection");
  }
  if (typeof threadOnly !== "boolean" || (threadOnly && (threadId === undefined || modelSelection !== undefined || updateBotDefault !== undefined))) {
    throw new Error("invalid thread-only approval selection");
  }
  if (typeof acknowledgeLocalAuto !== "boolean") {
    throw new Error("invalid local Auto acknowledgement");
  }
  if (modelSelection !== undefined && (mode !== "ask" || !plainObject(modelSelection) ||
    typeof modelSelection.instanceId !== "string" || typeof modelSelection.model !== "string" ||
    typeof updateBotDefault !== "boolean" || threadId === undefined)) {
    throw new Error("invalid confirmed model switch");
  }
  if (threadId !== undefined && (typeof threadId !== "string" || !BOT_ID.test(threadId) ||
    (!threadOnly && mode !== "full" && mode !== "custom" && modelSelection === undefined))) {
    throw new Error("invalid thread for trusted approval mode");
  }
  // Thread-scoped, so a lost reply cannot downgrade the bot default.
  if (typeof refreshPermissions !== "boolean" || (refreshPermissions && (!threadOnly || threadId === undefined || allThreads || modelSelection !== undefined))) {
    throw new Error("invalid permission refresh");
  }
  return {
    type: "approval-trusted-mode-set",
    requestId,
    botId,
    mode,
    ...(acknowledgeLocalAuto ? { acknowledgeLocalAuto: true } : {}),
    ...(threadId !== undefined ? { threadId } : {}),
    ...(threadOnly ? { threadOnly: true } : {}),
    ...(allThreads ? { allThreads: true } : {}),
    ...(refreshPermissions ? { refreshPermissions: true } : {}),
    ...(modelSelection !== undefined ? { modelSelection, updateBotDefault } : {}),
  };
}

function trustedApprovalModeConfirmation(requestId, botId, mode) {
  if (typeof requestId !== "string" || !REQUEST_ID.test(requestId)) {
    throw new Error("invalid trusted approval-mode confirmation id");
  }
  if (typeof botId !== "string" || !BOT_ID.test(botId)) {
    throw new Error("invalid bot id for trusted approval-mode confirmation");
  }
  if (mode !== "full" && mode !== "custom") {
    throw new Error("only elevated approval modes require confirmation");
  }
  return {
    type: "approval-trusted-mode-confirm",
    requestId,
    botId,
    mode,
  };
}

function trustedApprovalModeActivation(requestId, botId, mode) {
  const confirmation = trustedApprovalModeConfirmation(requestId, botId, mode);
  return { ...confirmation, type: "approval-trusted-mode-activate" };
}

function trustedApprovalModeFinalization(requestId, botId, mode) {
  const confirmation = trustedApprovalModeConfirmation(requestId, botId, mode);
  return { ...confirmation, type: "approval-trusted-mode-finalize" };
}

function trustedApprovalModeCommit(requestId, botId, mode) {
  const confirmation = trustedApprovalModeConfirmation(requestId, botId, mode);
  return { ...confirmation, type: "approval-trusted-mode-commit" };
}

function decodeTrustedApprovalModeResult(rawMessage) {
  const message = rawMessage?.data ?? rawMessage;
  if (!plainObject(message) || message.type !== "approval-trusted-mode-result") return null;
  if (typeof message.requestId !== "string" || !REQUEST_ID.test(message.requestId)) {
    throw new Error("invalid trusted approval-mode result id");
  }
  if (typeof message.ok !== "boolean") throw new Error("invalid trusted approval-mode result status");
  if (message.ok) {
    if (
      !plainObject(message.bot) ||
      typeof message.bot.id !== "string" ||
      !BOT_ID.test(message.bot.id) ||
      !APPROVAL_MODES.has(message.bot.approvalMode)
    ) {
      throw new Error("invalid bot in trusted approval-mode result");
    }
    return { requestId: message.requestId, ok: true, bot: message.bot };
  }
  if (message.error !== undefined && typeof message.error !== "string") {
    throw new Error("invalid trusted approval-mode result error");
  }
  const error = String(message.error ?? "The approval mode could not be enabled").trim();
  return {
    requestId: message.requestId,
    ok: false,
    error: (error || "The approval mode could not be enabled").slice(0, 300),
    ...(Object.hasOwn(ERROR_KEYS, message.errorKey) ? { errorKey: message.errorKey } : {}),
  };
}

/** The process object is part of the correlation key, so a stale or replacement child cannot answer another child's grant. */
function createTrustedApprovalModeCoordinator({ randomId, timeoutMs = 10_000, t = englishText.t } = {}) {
  if (typeof randomId !== "function") throw new Error("trusted approval-mode request id generator is unavailable");
  if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 60_000) {
    throw new Error("invalid trusted approval-mode request timeout");
  }
  const pending = new Map();
  const usedRequestIds = new Set();
  const latestRequestByBot = new Map();

  function nextMessage(botId, mode, acknowledgeLocalAuto = false, threadId, modelSelection, updateBotDefault, threadOnly = false, allThreads = false, refreshPermissions = false) {
    const message = trustedApprovalModeRequest(randomId(), botId, mode, acknowledgeLocalAuto, threadId, modelSelection, updateBotDefault, threadOnly, allThreads, refreshPermissions);
    if (usedRequestIds.has(message.requestId)) {
      throw new Error("Trusted approval-mode request id was reused");
    }
    usedRequestIds.add(message.requestId);
    return message;
  }

  function settle(requestId, action) {
    const request = pending.get(requestId);
    if (!request) return false;
    pending.delete(requestId);
    clearTimeout(request.timer);
    action(request);
    return true;
  }

  function request(proc, botId, mode, options = {}) {
    if (!plainObject(options)) {
      return Promise.reject(new Error("invalid trusted approval-mode options"));
    }
    let message;
    try {
      message = nextMessage(botId, mode, options.acknowledgeLocalAuto ?? false, options.threadId, options.modelSelection, options.updateBotDefault, options.threadOnly ?? false, options.allThreads ?? false, options.refreshPermissions ?? false);
    } catch (error) {
      return Promise.reject(error);
    }
    if (!proc || typeof proc.postMessage !== "function") {
      return Promise.reject(new Error("The embedded bot server is unavailable"));
    }

    return new Promise((resolve, reject) => {
      const timer = setTimeout(() => {
        failAmbiguously(
          message.requestId,
          new Error("The embedded bot server did not answer the approval-mode request"),
        );
      }, timeoutMs);
      timer.unref?.();
      pending.set(message.requestId, {
        proc,
        botId: message.botId,
        mode: message.mode,
        modelThreadId: message.modelSelection ? message.threadId : undefined,
        threadOnly: Boolean(message.threadOnly),
        allThreads: Boolean(message.allThreads),
        threadId: message.threadId,
        resolve,
        reject,
        timer,
        posted: false,
        phase: "setting",
        resultBot: null,
      });
      try {
        proc.postMessage(message);
        const waiting = pending.get(message.requestId);
        if (waiting) waiting.posted = true;
        latestRequestByBot.set(message.botId, message.requestId);
      } catch (error) {
        settle(message.requestId, ({ reject: rejectPending }) => {
          rejectPending(error instanceof Error ? error : new Error(String(error)));
        });
      }
    });
  }

  /** A Full/Custom request may be durable though its reply was lost: revoke it in order on the same port before failing. */
  function failAmbiguously(requestId, error) {
    settle(requestId, (waiting) => {
      const elevated = waiting.mode === "full" || waiting.mode === "custom";
      const isLatest = latestRequestByBot.get(waiting.botId) === requestId;
      if (!waiting.posted || !elevated || !isLatest) {
        waiting.reject(error);
        return;
      }
      request(waiting.proc, waiting.botId, "ask", waiting.allThreads ? { allThreads: true } : waiting.threadOnly ? { threadId: waiting.threadId, threadOnly: true } : {}).then(
        () => waiting.reject(error),
        (recoveryError) => waiting.reject(new Error(
          `${error instanceof Error ? error.message : String(error)}; the pending ${waiting.mode} selection could not be cleared: ${
            recoveryError instanceof Error ? recoveryError.message : String(recoveryError)
          }`,
        )),
      );
    });
  }

  function receive(proc, rawMessage) {
    const message = rawMessage?.data ?? rawMessage;
    if (!plainObject(message) || (
      message.type !== "approval-trusted-mode-result" &&
      message.type !== "approval-trusted-mode-confirm-result" &&
      message.type !== "approval-trusted-mode-activate-result" &&
      message.type !== "approval-trusted-mode-finalize-result" &&
      message.type !== "approval-trusted-mode-commit-result"
    )) return false;

    if (message.type === "approval-trusted-mode-commit-result") {
      const waiting = pending.get(message.requestId);
      if (!waiting || waiting.proc !== proc || waiting.phase !== "committing") return true;
      const taskMode = message.bot?.tasks?.find(task => task.threadId === waiting.threadId)?.approvalMode;
      const matches = waiting.allThreads
        ? message.bot?.approvalMode === waiting.mode && Array.isArray(message.bot?.tasks) &&
          message.bot.tasks.every(task => task.approvalMode === waiting.mode)
        : taskMode === waiting.mode;
      if (message.ok !== true || message.bot?.id !== waiting.botId || !matches) {
        failAmbiguously(message.requestId, new Error("The thread approval change was not committed"));
      } else settle(message.requestId, ({ resolve }) => resolve(message.bot));
      return true;
    }

    if (message.type !== "approval-trusted-mode-result") {
      const steps = {
        "approval-trusted-mode-confirm-result": {
          phase: "confirming",
          nextPhase: "activating",
          nextMessage: trustedApprovalModeActivation,
        },
        "approval-trusted-mode-activate-result": {
          phase: "activating",
          nextPhase: "finalizing",
          nextMessage: trustedApprovalModeFinalization,
        },
        "approval-trusted-mode-finalize-result": {
          phase: "finalizing",
          nextPhase: null,
          nextMessage: null,
        },
      };
      const step = steps[message.type];
      const requestId = typeof message.requestId === "string" ? message.requestId : "";
      const waiting = pending.get(requestId);
      if (!REQUEST_ID.test(requestId) || typeof message.ok !== "boolean") {
        if (waiting?.proc === proc && waiting.phase === step.phase) {
          failAmbiguously(requestId, new Error("invalid trusted approval-mode handshake result"));
        }
        return true;
      }
      if (!waiting || waiting.proc !== proc || waiting.phase !== step.phase) return true;
      if (!message.ok) {
        const error = typeof message.error === "string" && message.error.trim()
          ? message.error.trim().slice(0, 300)
          : "The approval mode handshake was refused";
        settle(requestId, ({ reject }) => reject(new Error(error)));
        return true;
      }
      if (!step.nextMessage || !step.nextPhase) {
        // The ACK leaves a durable `committed` journal inert; startup revokes a surviving journal to Ask, so a failure never leaves the bot elevated.
        try {
          if (waiting.threadOnly || waiting.allThreads) waiting.phase = "committing";
          proc.postMessage(trustedApprovalModeCommit(
            requestId,
            waiting.botId,
            waiting.mode,
          ));
        } catch (error) {
          failAmbiguously(requestId, error instanceof Error ? error : new Error(String(error)));
          return true;
        }
        if (!waiting.threadOnly && !waiting.allThreads) settle(requestId, ({ resolve, resultBot }) => resolve(resultBot));
        return true;
      }
      try {
        waiting.phase = step.nextPhase;
        proc.postMessage(step.nextMessage(
          requestId,
          waiting.botId,
          waiting.mode,
        ));
      } catch (error) {
        failAmbiguously(requestId, error instanceof Error ? error : new Error(String(error)));
        return true;
      }
      return true;
    }

    let result;
    try {
      result = decodeTrustedApprovalModeResult(message);
    } catch (error) {
      const requestId = typeof message.requestId === "string" ? message.requestId : "";
      const waiting = pending.get(requestId);
      if (waiting?.proc === proc) failAmbiguously(requestId, error);
      return true;
    }

    const waiting = pending.get(result.requestId);
    // Consume late, duplicated or foreign answers, but never let one cross processes.
    if (!waiting || waiting.proc !== proc || waiting.phase !== "setting") return true;
    if (!result.ok) {
      const key = ERROR_KEYS[result.errorKey];
      const text = key && result.error === englishText.t(key) ? t(key) : result.error;
      settle(result.requestId, ({ reject }) => reject(new Error(text)));
      return true;
    }
    const resultMode = waiting.threadOnly && (waiting.mode === "full" || waiting.mode === "custom")
      ? waiting.mode // Inert prepare reply; validate the actual thread at commit.
      : waiting.threadOnly ? result.bot.tasks?.find(task => task.threadId === waiting.threadId)?.approvalMode
      : waiting.modelThreadId
      ? result.bot.tasks?.find((task) => task.threadId === waiting.modelThreadId)?.approvalMode
      : result.bot.approvalMode;
    if (result.bot.id !== waiting.botId || resultMode !== waiting.mode) {
      failAmbiguously(result.requestId, new Error("Approval-mode result did not match the request"));
      return true;
    }
    if (waiting.mode === "full" || waiting.mode === "custom") {
      try {
        // Resolve only after the correlated acknowledgement, once activation is queued on this ordered port.
        waiting.phase = "confirming";
        waiting.resultBot = result.bot;
        proc.postMessage(trustedApprovalModeConfirmation(
          result.requestId,
          waiting.botId,
          waiting.mode,
        ));
      } catch (error) {
        failAmbiguously(
          result.requestId,
          error instanceof Error ? error : new Error(String(error)),
        );
        return true;
      }
      return true;
    }
    settle(result.requestId, ({ resolve }) => resolve(result.bot));
    return true;
  }

  function rejectProcess(proc, reason = "The embedded bot server stopped") {
    const error = reason instanceof Error ? reason : new Error(String(reason));
    for (const [requestId, request] of pending) {
      if (request.proc !== proc) continue;
      settle(requestId, ({ reject }) => reject(error));
    }
  }

  return { request, receive, rejectProcess };
}

module.exports = {
  createTrustedApprovalModeCoordinator,
  decodeTrustedApprovalModeResult,
  trustedApprovalModeActivation,
  trustedApprovalModeCommit,
  trustedApprovalModeConfirmation,
  trustedApprovalModeFinalization,
  trustedApprovalModeRequest,
};