openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
188 lines (187 loc) • 11 kB
JavaScript
import { o as resolveUserPath } from "./home-dir-Dc6nN6HB.mjs";
import { c as isRecord, o as asRecord } from "./record-coerce-TIsquoVR.mjs";
import { d as normalizeStringEntries } from "./string-normalization-CECOqf64.mjs";
import "./utils-DgZn2dLu.mjs";
import { u as tryReadJsonSync } from "./json-files-C5mNTScg.mjs";
import { o as parseRegistryNpmSpec } from "./npm-registry-spec-Dw2vyAJw.mjs";
import { c as tracePluginLifecyclePhaseAsync } from "./discovery-Dma3bVNk.mjs";
import { r as loadPluginManifest } from "./manifest-BXjFCp0F.mjs";
import { l as listOfficialExternalPluginCatalogEntries, p as resolveOfficialExternalPluginInstall } from "./official-external-plugin-catalog-Dcv926GC.mjs";
import { r as loadInstalledPluginIndexInstallRecords } from "./installed-plugin-index-record-reader-BP4r74yH.mjs";
import { g as resolveOfficialExternalPluginId } from "./official-external-plugin-catalog-source-B7ojvzQW.mjs";
import { u as readConfigFileSnapshotForWrite } from "./io.runtime-BsUXSsD4.mjs";
import "./config-DLycWFvB.mjs";
import "./installed-plugin-index-records-D8YKQ3gZ.mjs";
import { t as findBundledPluginSource } from "./bundled-sources-VMphqOte.mjs";
import { n as listPersistedBundledPluginRecoveryLocations } from "./location-bridges-CFcxC8em.mjs";
import { a as supportsInstallConfigSingleTopLevelIncludeShape, i as selectInstallMutationWriteOptions, r as resolveInstallConfigMutationPreflights } from "./install-config-mutation-CIWDvRTm.mjs";
import { a as resolveBundledInstallPlanBeforeNpm, i as parseNpmPrefixSpec, o as resolveFileNpmSpecToLocalPath } from "./install-source-spec-CPEKmm8R.mjs";
import fs from "node:fs";
import path from "node:path";
//#region src/plugins/install-config.ts
function resolveFullyBlockedConfigMutationReason(snapshot) {
if (snapshot.pluginMutation.mode !== "blocked" || snapshot.hookMutation.mode !== "blocked") return null;
if (snapshot.pluginMutation.reason === snapshot.hookMutation.reason) return snapshot.pluginMutation.reason;
return `Config plugin and hook mutations are both blocked. ${snapshot.pluginMutation.reason} ${snapshot.hookMutation.reason}`;
}
var PluginInstallConfigError = class extends Error {
constructor(message, blockedSnapshot) {
super(message);
this.blockedSnapshot = blockedSnapshot;
this.code = "INVALID_CONFIG";
this.name = "PluginInstallConfigError";
}
};
function extractMissingPluginLoadPath(issue) {
if (issue.path !== "plugins.load.paths") return null;
const markerIndex = issue.message.indexOf("plugin path not found:");
if (markerIndex < 0) return null;
return issue.message.slice(markerIndex + 22).trim() || null;
}
function isOwnedMissingPluginLoadPathIssue(issue, ownedLoadPaths, env = process.env) {
const missingPath = extractMissingPluginLoadPath(issue);
return missingPath !== null && ownedLoadPaths.has(resolveUserPath(missingPath, env));
}
function isAllowedPluginRecoveryIssue(issue, request, ownedLoadPaths) {
const pluginId = request.bundledPluginId?.trim();
if (!pluginId) return false;
return issue.path === `channels.${pluginId}` && issue.message === `unknown channel id: ${pluginId}` || issue.path.startsWith("channels.") && issue.message.startsWith(`invalid config for plugin ${pluginId}:`) || isOwnedMissingPluginLoadPathIssue(issue, ownedLoadPaths) || issue.path === `plugins.entries.${pluginId}` && issue.message.includes("requires compiled runtime output") || issue.path === "tools.web.search.provider" && issue.message.includes(`plugin "${pluginId}"`);
}
function removeOwnedMissingPluginLoadPaths(cfg, issues, ownedLoadPaths, env) {
const missingPaths = /* @__PURE__ */ new Set();
for (const issue of issues) {
const missingPath = extractMissingPluginLoadPath(issue);
if (!missingPath) continue;
const resolved = resolveUserPath(missingPath, env);
if (ownedLoadPaths.has(resolved)) missingPaths.add(resolved);
}
const paths = cfg.plugins?.load?.paths;
if (missingPaths.size === 0 || !Array.isArray(paths)) return cfg;
const nextPaths = paths.filter((entry) => typeof entry !== "string" || !missingPaths.has(resolveUserPath(entry, env)));
if (nextPaths.length === paths.length) return cfg;
return {
...cfg,
plugins: {
...cfg.plugins,
load: {
...cfg.plugins?.load,
paths: nextPaths
}
}
};
}
async function resolveRequestedPluginInstallPaths(cfg, issues, request, env) {
if (!issues.some((issue) => extractMissingPluginLoadPath(issue) !== null)) return /* @__PURE__ */ new Set();
const installRecords = await loadInstalledPluginIndexInstallRecords();
const ownedLoadPaths = /* @__PURE__ */ new Set();
const pluginId = request.bundledPluginId?.trim();
if (!pluginId) return ownedLoadPaths;
const record = installRecords[pluginId] ?? cfg.plugins?.installs?.[pluginId];
for (const value of [record?.sourcePath, record?.installPath]) if (typeof value === "string" && value.trim()) ownedLoadPaths.add(resolveUserPath(value, env));
if (issues.some((issue) => extractMissingPluginLoadPath(issue) !== null && !isOwnedMissingPluginLoadPathIssue(issue, ownedLoadPaths, env))) {
const loadPaths = (await listPersistedBundledPluginRecoveryLocations({ env })).filter((location) => location.pluginId === pluginId).flatMap((location) => location.loadPaths);
for (const loadPath of loadPaths) ownedLoadPaths.add(resolveUserPath(loadPath, env));
}
return ownedLoadPaths;
}
async function recoverPluginInstallConfig(request, snapshot) {
if (resolvePluginInstallInvalidConfigPolicy(request) !== "allow-plugin-recovery") throw new PluginInstallConfigError("Config invalid; run `openclaw doctor --fix` before installing plugins.");
const parsed = snapshot.parsed ?? {};
if (!snapshot.exists || Object.keys(parsed).length === 0) throw new PluginInstallConfigError("Config file could not be parsed; run `openclaw doctor` to repair it.");
const ownedLoadPaths = await resolveRequestedPluginInstallPaths(snapshot.config, snapshot.issues, request, process.env);
if (snapshot.legacyIssues.length > 0 || snapshot.issues.length === 0 || snapshot.issues.some((issue) => !isAllowedPluginRecoveryIssue(issue, request, ownedLoadPaths))) throw new PluginInstallConfigError(`Config invalid outside the plugin recovery path for ${request.bundledPluginId ?? "the requested plugin"}; run \`openclaw doctor --fix\` before reinstalling it.`);
if (Object.hasOwn(parsed, "$include") || !supportsInstallConfigSingleTopLevelIncludeShape(isRecord(parsed) ? parsed.plugins : void 0)) throw new PluginInstallConfigError("Config plugin recovery uses an unsupported $include shape; use a single-file top-level plugins include or run `openclaw doctor --fix` before reinstalling it.");
return removeOwnedMissingPluginLoadPaths(snapshot.config, snapshot.issues, ownedLoadPaths, process.env);
}
/** Read and authorize install configuration only after mutation-free request preflight. */
async function loadConfigForInstall(request) {
const { snapshot, writeOptions } = await tracePluginLifecyclePhaseAsync("config read", () => readConfigFileSnapshotForWrite(), { command: "install" });
const mutationWriteOptions = selectInstallMutationWriteOptions(writeOptions);
const config = snapshot.valid ? snapshot.sourceConfig : await recoverPluginInstallConfig(request, snapshot);
const parsed = asRecord(snapshot.parsed);
const { hookMutation, pluginMutation } = resolveInstallConfigMutationPreflights({
parsed,
snapshotPath: snapshot.path,
writeOptions: mutationWriteOptions
});
const resolved = {
config,
baseHash: snapshot.hash,
writeOptions: mutationWriteOptions,
hookMutation,
pluginMutation
};
if ((!snapshot.valid || request.installKind === "plugin") && pluginMutation.mode === "blocked") throw new PluginInstallConfigError(pluginMutation.reason, resolved);
return resolved;
}
function readPluginInstallRecoveryMetadata(rootDir) {
const packageJsonPath = path.join(rootDir, "package.json");
if (!fs.existsSync(packageJsonPath)) return { allowInvalidConfigRecovery: false };
const manifest = loadPluginManifest(rootDir, false);
const pluginId = manifest.ok ? manifest.manifest.id : void 0;
const parsed = tryReadJsonSync(packageJsonPath);
return {
...pluginId ? { pluginId } : {},
allowInvalidConfigRecovery: parsed?.openclaw?.install?.allowInvalidConfigRecovery === true
};
}
function resolvePluginInstallRecoveryMetadata(rawSpec, localPath) {
if (localPath !== void 0) {
const direct = readPluginInstallRecoveryMetadata(localPath);
return direct.pluginId || direct.allowInvalidConfigRecovery ? direct : {};
}
const npmPrefixSpec = parseNpmPrefixSpec(rawSpec);
const values = new Set(normalizeStringEntries([
rawSpec,
npmPrefixSpec ?? "",
parseRegistryNpmSpec(rawSpec)?.name ?? "",
npmPrefixSpec ? parseRegistryNpmSpec(npmPrefixSpec)?.name : ""
]));
if (values.size === 0) return {};
for (const entry of listOfficialExternalPluginCatalogEntries()) {
const install = resolveOfficialExternalPluginInstall(entry);
const npmSpec = install?.npmSpec?.trim() || entry.name?.trim();
if (!npmSpec || !values.has(npmSpec)) continue;
const pluginId = resolveOfficialExternalPluginId(entry);
return {
...pluginId ? { pluginId } : {},
allowInvalidConfigRecovery: install?.allowInvalidConfigRecovery === true
};
}
return {};
}
/** Resolve install metadata from the raw spec before Commander action handlers mutate config. */
function resolvePluginInstallRequestContext(params) {
if (params.marketplace) return {
ok: true,
request: {
rawSpec: params.rawSpec,
installKind: "plugin",
marketplace: params.marketplace
}
};
const fileSpec = resolveFileNpmSpecToLocalPath(params.rawSpec);
if (fileSpec && !fileSpec.ok) return fileSpec;
const normalizedSpec = fileSpec?.path ?? params.rawSpec;
const resolvedPath = resolveUserPath(params.localPath ?? normalizedSpec);
const localPath = params.source ? params.source === "local" || params.source === "bundled" ? resolvedPath : void 0 : fileSpec || fs.existsSync(resolvedPath) ? resolvedPath : resolveBundledInstallPlanBeforeNpm({
rawSpec: params.rawSpec,
findBundledSource: (lookup) => findBundledPluginSource({ lookup })
})?.bundledSource.localPath;
const recovered = resolvePluginInstallRecoveryMetadata(params.rawSpec, localPath);
return {
ok: true,
request: {
rawSpec: params.rawSpec,
...params.installKind === "plugin" || recovered.pluginId ? { installKind: "plugin" } : {},
...recovered.pluginId ? { bundledPluginId: recovered.pluginId } : {},
...recovered.allowInvalidConfigRecovery !== void 0 ? { allowInvalidConfigRecovery: recovered.allowInvalidConfigRecovery } : {}
}
};
}
/** Decide whether invalid config should block a command before plugin recovery can run. */
function resolvePluginInstallInvalidConfigPolicy(request) {
return request?.allowInvalidConfigRecovery === true ? "allow-plugin-recovery" : "deny";
}
//#endregion
export { resolvePluginInstallRequestContext as a, resolvePluginInstallInvalidConfigPolicy as i, loadConfigForInstall as n, resolveFullyBlockedConfigMutationReason as r, PluginInstallConfigError as t };