UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

543 lines (542 loc) • 23.2 kB
import { o as resolveUserPath } from "./home-dir-Dc6nN6HB.mjs"; import { c as isRecord } from "./record-coerce-TIsquoVR.mjs"; import { y as uniqueStrings } from "./string-normalization-CECOqf64.mjs"; import { r as defaultRuntime } from "./runtime-DlO3PTAd.mjs"; import { p as shortenHomePath } from "./utils-DgZn2dLu.mjs"; import { t as formatErrorMessage } from "./errors-O33D01XW.mjs"; import { t as formatCliCommand } from "./command-format-DwEdrDKL.mjs"; import { r as theme } from "./theme-DzaUZY4q.mjs"; import { t as CLAWHUB_INSTALL_ERROR_CODE } from "./clawhub-error-codes-DV-j2dZ7.mjs"; import { t as PLUGIN_INSTALL_ERROR_CODE } from "./install-types-auyGAGw8.mjs"; import { c as tracePluginLifecyclePhaseAsync } from "./discovery-Dma3bVNk.mjs"; import { r as assertConfigWriteAllowedInCurrentMode } from "./config-write-guard-6uPuICvJ.mjs"; import { t as ManagedPluginLifecycleError } from "./management-lifecycle-error-ySeX1uI2.mjs"; import { r as replaceConfigFile } from "./mutate-DJq1JylX.mjs"; import "./config-DLycWFvB.mjs"; import { i as withPluginLifecycleLease, t as hasPluginLifecycleLease } from "./plugin-lifecycle-lease-DHQMWweY.mjs"; import { f as resolveArchiveKind } from "./archive-DqZZMnnW.mjs"; import { t as buildNpmResolutionFields } from "./install-source-utils-UowE37De.mjs"; import { n as readInstallPolicyWarningErrorDetails } from "./install-policy-warning-error-details-D7CwLpKg.mjs"; import { a as resolvePackageDirInstallTransaction, i as requestDeferredPackageDirInstall } from "./install-package-dir-Bloi0nzp.mjs"; import "./install-dM32T0FP.mjs"; import { t as findBundledPluginSource } from "./bundled-sources-VMphqOte.mjs"; import "./clawhub-DobIBpe5.mjs"; import { r as resolveMarketplaceInstallShortcut } from "./marketplace-D_TgHrzw.mjs"; import { r as promptYesNo } from "./prompt-Dtcnfqvp.mjs"; import { n as resolvePluginCapabilityConsentCliOptions } from "./plugin-capability-consent-DLd1Yi8I.mjs"; import { n as formatNonClawHubInstallWarning, t as NON_CLAWHUB_INSTALL_FORCE_FLAG } from "./install-provenance-CwhW_ROz.mjs"; import { a as installHooksFromNpmSpec, i as formatPluginInstallWithHookFallbackError, n as createPluginInstallLogger, o as installHooksFromPath, r as enableInternalHookEntries, t as createHookPackInstallLogger } from "./plugins-command-helpers-eRcd7QEj.mjs"; import { a as resolvePluginInstallSourcePlan, r as resolveBundledInstallPlanForNpmFailure } from "./install-source-plan-CHGWf7Qw.mjs"; import { a as resolvePluginInstallRequestContext, n as loadConfigForInstall, r as resolveFullyBlockedConfigMutationReason, t as PluginInstallConfigError } from "./install-config-CXmP2uuQ.mjs"; import { t as PluginInstallPersistedError } from "./lifecycle--64VzzoC.mjs"; import { t as installManagedPlugin } from "./management-mutations-TU1AYAYB.mjs"; import { t as resolveClawHubInstallConfirmation } from "./clawhub-install-confirmation-D63kIae-.mjs"; import { n as resolvePluginLifecycleGateway } from "./plugins-lifecycle-client-CvQJuvmG.mjs"; import { t as resolveInstallPolicyWarningAcknowledgementCliOptions } from "./install-policy-warning-acknowledgement-CzVVj90y.mjs"; import { t as stageHookInstall } from "./install-record-transaction-DBAa8uF8.mjs"; import fs from "node:fs"; //#region src/cli/non-clawhub-install-acknowledgement.ts async function confirmNonClawHubInstall(params) { const warning = formatNonClawHubInstallWarning({ sourceClass: params.sourceClass, spec: params.spec }); if (params.acknowledged) { params.runtime.log(theme.warn(warning)); return true; } if (process.stdin.isTTY && process.stdout.isTTY) { params.runtime.log(theme.warn(warning)); return await promptYesNo("Install this non-ClawHub plugin source?"); } params.runtime.error(`${warning}\nInstall cancelled; rerun with ${NON_CLAWHUB_INSTALL_FORCE_FLAG} after reviewing the source.`); return false; } //#endregion //#region src/cli/plugins-install-gateway.ts /** Keep CLI prompts local; both transports use the same public install request. */ function createGatewayPluginInstaller(gateway) { return async (params) => { if (params.request.source === "clawhub" && params.request.mode !== "update" && params.confirmInstall && !await params.confirmInstall()) throw new ManagedPluginLifecycleError("Install cancelled.", { installRejected: true }); params.signal?.throwIfAborted(); params.beforePersistentApply?.(); let result; try { try { result = await gateway("plugins.install", params.request, params.onCapabilityConsent); } catch (error) { const details = readInstallPolicyWarningErrorDetails(error instanceof Error && "details" in error ? error.details : void 0); if (!details || !params.safetyOverrides?.onInstallPolicyWarning || (await params.safetyOverrides.onInstallPolicyWarning(details)).status !== "approved") throw error; result = await gateway("plugins.install", { ...params.request, acknowledgeInstallPolicyWarning: true }, params.onCapabilityConsent); } } catch (error) { const details = error instanceof Error && "details" in error ? error.details : void 0; const saved = isRecord(details) ? details.persistence : void 0; if (isRecord(saved) && saved.operation === "install" && typeof saved.pluginId === "string") throw new PluginInstallPersistedError(saved.pluginId, error); if (isRecord(details) && details.pluginInstallRejected === true) { const source = details.pluginInstallSource; const installSource = isRecord(source) && (source.source === "npm" || source.source === "clawhub") && typeof source.spec === "string" ? { source: source.source, spec: source.spec, ...typeof source.expectedIntegrity === "string" ? { expectedIntegrity: source.expectedIntegrity } : {} } : void 0; throw new ManagedPluginLifecycleError(error instanceof Error ? error.message : String(error), { installRejected: true, installSource, ...typeof details.pluginInstallCode === "string" ? { code: details.pluginInstallCode } : {}, cause: error }); } throw error; } return { plugin: result.plugin, warnings: result.warnings, application: result.runtime }; }; } //#endregion //#region src/cli/hook-install-persistence.ts async function persistHookPackInstall(params) { const runtime = params.runtime ?? defaultRuntime; return await withPluginLifecycleLease({}, async (lease) => { const assertPersistentApply = () => { lease.assertOwned(); params.snapshot.writeOptions.assertConfigPathForWrite?.(); params.beforePersistentApply?.(); }; const next = enableInternalHookEntries(params.snapshot.config, params.hooks); const transaction = await stageHookInstall({ update: { hookId: params.hookPackId, hooks: params.hooks, ...params.install }, payloadTransaction: params.payloadTransaction, lease, beforePersistentApply: assertPersistentApply }); try { await replaceConfigFile({ nextConfig: next, baseHash: params.snapshot.baseHash, writeOptions: { ...params.snapshot.writeOptions, assertConfigPathForWrite: assertPersistentApply } }); } catch (error) { try { await transaction.rollback(); } catch (rollbackError) { throw new AggregateError([error, rollbackError], "Hook install config rollback failed", { cause: rollbackError }); } throw error; } await transaction.commit(); runtime.log(params.successMessage ?? `Installed hook pack: ${params.hookPackId}`); runtime.log("Hook install/link config can activate immediately in hybrid mode; code-only updates and reload mode off need a Gateway restart."); return next; }); } //#endregion //#region src/cli/npm-resolution.ts /** CLI adapter for npm install-record pinning with styled warning output. */ function resolvePinnedNpmInstallRecordForCli(rawSpec, pin, installPath, version, resolution, log, warnFormat) { const resolvedSpec = resolution?.resolvedSpec; const recordSpec = pin && resolvedSpec ? resolvedSpec : rawSpec; if (pin) { if (resolvedSpec) log(`Pinned npm install record to ${resolvedSpec}.`); else log(warnFormat("Could not resolve exact npm version for --pin; storing original npm spec.")); } return { source: "npm", spec: recordSpec, installPath, version, ...buildNpmResolutionFields(resolution) }; } //#endregion //#region src/cli/plugins-install-hook-fallback.ts function resolveInstallSafetyOverrides(overrides) { return { config: overrides.config, onInstallPolicyWarning: overrides.onInstallPolicyWarning, trustedSourceLinkedOfficialInstall: overrides.trustedSourceLinkedOfficialInstall }; } async function attemptHookInstall(source, params, options, assertOwned) { const common = requestDeferredPackageDirInstall({ ...resolveInstallSafetyOverrides(params.safetyOverrides ?? {}), config: params.snapshot.config, mode: source.mode, logger: createHookPackInstallLogger(params.runtime), ...options }, assertOwned); try { return source.source === "local" ? await installHooksFromPath({ ...common, path: source.path, ...source.link ? { dryRun: true } : {} }) : await installHooksFromNpmSpec({ ...common, spec: source.spec, ...source.expectedIntegrity ? { expectedIntegrity: source.expectedIntegrity } : {} }); } catch (error) { return { ok: false, error: formatErrorMessage(error) }; } } async function installHookPack(source, params, expectedPackageKind) { if (params.request.enable === false) return { ok: false, error: "--no-enable is only supported for plugins. Install hook packs separately with openclaw hooks install." }; return await withPluginLifecycleLease({ signal: params.signal }, async (lease) => { const request = resolvePluginInstallRequestContext({ rawSpec: source.source === "local" ? source.path : source.spec }); if (!request.ok) return request; const snapshot = await loadConfigForInstall(request.request).catch((error) => { if (expectedPackageKind === "hook-only" && error instanceof PluginInstallConfigError && error.blockedSnapshot) return error.blockedSnapshot; throw error; }); if (snapshot.hookMutation.mode === "blocked") return { ok: false, error: snapshot.hookMutation.reason }; const linked = source.source === "local" && source.link; if (linked && !fs.statSync(source.path).isDirectory()) return { ok: false, error: "Linked hook pack paths must be directories." }; const beforePersistentApply = () => { params.signal?.throwIfAborted(); lease.assertOwned(); snapshot.writeOptions.assertConfigPathForWrite?.(); params.beforePersistentApply?.(); }; const result = await attemptHookInstall(source, { ...params, snapshot }, { expectedPackageKind, beforePersistentApply }, lease.assertOwned.bind(lease)); if (!result.ok) return result; const runtime = params.runtime ?? defaultRuntime; const config = snapshot.config; const pinMessages = []; await persistHookPackInstall({ snapshot: linked ? { ...snapshot, config: { ...config, hooks: { ...config.hooks, internal: { ...config.hooks?.internal, load: { ...config.hooks?.internal?.load, extraDirs: uniqueStrings([...config.hooks?.internal?.load?.extraDirs ?? [], source.path]) } } } } } : snapshot, hookPackId: result.hookPackId, hooks: result.hooks, install: source.source === "local" ? { source: resolveArchiveKind(source.path) ? "archive" : "path", sourcePath: source.path, installPath: linked ? source.path : result.targetDir, version: result.version } : resolvePinnedNpmInstallRecordForCli(source.spec, Boolean(source.pin), result.targetDir, result.version, result.npmResolution, (message) => pinMessages.push(message), theme.warn), ...linked ? { successMessage: `Linked hook pack path: ${shortenHomePath(source.path)}` } : {}, runtime, beforePersistentApply, payloadTransaction: resolvePackageDirInstallTransaction(result) }); for (const message of pinMessages) runtime.log(message); return { ok: true }; }); } async function installInspectedHookPack(source, params) { const probe = await attemptHookInstall(source, params, { inspection: "package-kind" }); if (!probe.ok || probe.packageKind !== "hook-only") return; return await installHookPack(source.source !== "local" && probe.npmResolution?.integrity ? { ...source, expectedIntegrity: probe.npmResolution.integrity } : source, params, "hook-only"); } /** Every source uses the same plugin executor; hook fallback needs an eligible artifact. */ async function installPluginWithHookFallback(params) { const { request, snapshot, install: execute = installManagedPlugin, ...options } = params; const compatible = request.source === "local" || request.source === "npm" ? request : void 0; const blocked = resolveFullyBlockedConfigMutationReason(snapshot); if (blocked) return { ok: false, error: blocked }; if (compatible) { if (snapshot.pluginMutation.mode === "blocked" || snapshot.hookMutation.mode === "blocked") { const hook = await installInspectedHookPack(compatible, params); if (hook) return hook; if (snapshot.pluginMutation.mode === "blocked") return { ok: false, error: snapshot.pluginMutation.reason }; } } const install = async (installRequest) => { const runtime = params.runtime ?? defaultRuntime; const logWarning = options.logger?.warn ?? createPluginInstallLogger(runtime).warn; const warnings = /* @__PURE__ */ new Set(); const warn = (message) => { if (!warnings.has(message)) { warnings.add(message); logWarning(message); } }; try { const result = await execute({ ...options, logger: { ...options.logger, warn }, request: installRequest }); for (const warning of result.warnings ?? []) warn(warning); runtime.log(installRequest.source === "local" && installRequest.link ? `Linked plugin path: ${shortenHomePath(installRequest.path)}` : `Installed plugin: ${result.plugin.id}`); runtime.log(result.application ? `Applied in Gateway generation ${result.application.generation}.` : "Saved for the next Gateway start."); return { ok: true }; } catch (error) { if (!(error instanceof ManagedPluginLifecycleError) || !error.installRejected) throw error; return { ok: false, error: error.message, code: error.code, warning: error.warning, installSource: error.installSource }; } }; const result = await install(request); if (result.ok) return result; const selectedSource = result.installSource; const hookSource = request.source === "official" && (result.code === PLUGIN_INSTALL_ERROR_CODE.MISSING_OPENCLAW_EXTENSIONS || result.code === PLUGIN_INSTALL_ERROR_CODE.CONFIG_MUTATION_BLOCKED) && selectedSource?.source === "npm" ? { source: "npm", spec: selectedSource.spec, expectedIntegrity: selectedSource.expectedIntegrity, mode: request.mode, pin: request.pin } : compatible; if (result.code === PLUGIN_INSTALL_ERROR_CODE.CONFIG_MUTATION_BLOCKED) return (hookSource && await installInspectedHookPack(hookSource, params)) ?? result; if (!hookSource || [ PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED, PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_FAILED, PLUGIN_INSTALL_ERROR_CODE.RELEASE_COHORT_UNAVAILABLE, PLUGIN_INSTALL_ERROR_CODE.UNSUPPORTED_PLAIN_FILE_PLUGIN ].some((code) => code === result.code)) return result; if (request.source === "npm" && params.allowBundledFallback) { const fallback = resolveBundledInstallPlanForNpmFailure({ rawSpec: request.spec, code: result.code, findBundledSource: (lookup) => findBundledPluginSource({ lookup }) }); if (fallback) { (params.runtime ?? defaultRuntime).log(theme.warn(fallback.warning)); return await install({ source: "bundled", pluginId: fallback.bundledSource.pluginId, ...request.enable === false ? { enable: false } : {} }); } } const hook = await installHookPack(hookSource, params); return hook.ok ? hook : { ...result, error: formatPluginInstallWithHookFallbackError(result.error, hook) }; } //#endregion //#region src/cli/plugins-install-preflight.ts function resolveMarketplaceOptionError(opts) { if (opts.link) return `--link is not supported with --marketplace. Remove --link, or install a local path with ${formatCliCommand(`openclaw plugins install --link <path> ${NON_CLAWHUB_INSTALL_FORCE_FLAG}`)}.`; if (opts.pin) return `--pin is not supported with --marketplace. Use ${formatCliCommand(`openclaw plugins install <plugin> --marketplace <name> ${NON_CLAWHUB_INSTALL_FORCE_FLAG}`)} without --pin.`; return null; } function resolveSourceOptionError(opts, sourcePlan) { if (sourcePlan.request.source === "git" && opts.link) return `--link is not supported with git: installs. Use ${formatCliCommand(`openclaw plugins install git:<repo>@<ref> ${NON_CLAWHUB_INSTALL_FORCE_FLAG}`)} for Git installs or ${formatCliCommand(`openclaw plugins install --link <path> ${NON_CLAWHUB_INSTALL_FORCE_FLAG}`)} for local paths.`; if (sourcePlan.request.source === "git" && opts.pin) return `--pin is not supported with git: installs. Pin the ref in the spec instead, for example ${formatCliCommand(`openclaw plugins install git:<repo>@<ref> ${NON_CLAWHUB_INSTALL_FORCE_FLAG}`)}.`; if (opts.pin && sourcePlan.request.source !== "npm" && sourcePlan.request.source !== "official" && sourcePlan.request.source !== "bundled") return "--pin is only supported with npm registry installs."; if (opts.link && sourcePlan.request.source !== "local") return `--link requires a local path. Run ${formatCliCommand(`openclaw plugins install --link <path> ${NON_CLAWHUB_INSTALL_FORCE_FLAG}`)}.`; return null; } /** Complete source and option validation before acquiring the persistent lifecycle lease. */ async function resolvePluginInstallPreflight(params) { if (!params.raw.trim()) return { ok: false, error: "Plugin install source must not be empty." }; if (params.opts.marketplace !== void 0 && !params.opts.marketplace.trim()) return { ok: false, error: "--marketplace requires a non-empty source." }; const installMode = params.opts.force && !params.opts.link ? "update" : "install"; let raw = params.raw; let marketplace = params.opts.marketplace; let sourcePlan = null; if (marketplace === void 0) { const shorthand = await tracePluginLifecyclePhaseAsync("marketplace shortcut resolution", () => resolveMarketplaceInstallShortcut(raw), { command: "install" }); if (shorthand?.ok === false) return { ok: false, error: shorthand.error }; if (shorthand?.ok) { raw = shorthand.plugin; marketplace = shorthand.marketplaceSource; } else { const planned = resolvePluginInstallSourcePlan({ raw, mode: installMode, link: params.opts.link, pin: params.opts.pin }); if (!planned.ok) return planned; sourcePlan = planned; } } if (marketplace && fs.existsSync(resolveUserPath(marketplace))) marketplace = resolveUserPath(marketplace); const opts = { ...params.opts, marketplace }; const optionError = marketplace ? resolveMarketplaceOptionError(opts) : sourcePlan ? resolveSourceOptionError(opts, sourcePlan) : "Plugin install source could not be resolved."; if (optionError) return { ok: false, error: optionError }; const requestResolution = resolvePluginInstallRequestContext({ rawSpec: raw, marketplace, source: sourcePlan?.request.source, localPath: sourcePlan?.localPath }); if (!requestResolution.ok) return requestResolution; const source = sourcePlan?.request.source; const request = source && [ "npm-pack", "git", "clawhub", "bundled", "official" ].includes(source) ? { ...requestResolution.request, installKind: "plugin" } : requestResolution.request; if (marketplace) return { ok: true, raw, opts, installMode, request, marketplace, sourcePlan: null }; if (!sourcePlan) return { ok: false, error: "Plugin install source could not be resolved." }; return { ok: true, raw, opts, installMode, request, sourcePlan }; } //#endregion //#region src/cli/plugins-install-command.ts const DEPRECATED_DANGEROUS_FORCE_UNSAFE_INSTALL_WARNING = "--dangerously-force-unsafe-install is deprecated and no longer affects plugin installs because built-in install-time dangerous-code scanning has been removed. Configure security.installPolicy for operator-owned install decisions."; /** Validate intent and select the runtime owner before either process acquires its lease. */ async function runPluginInstallCommand(params) { assertConfigWriteAllowedInCurrentMode(); const runtime = params.runtime ?? defaultRuntime; const preflight = await resolvePluginInstallPreflight(params); if (!preflight.ok) { runtime.error(preflight.error); return runtime.exit(1); } const { raw, opts, sourcePlan } = preflight; if (opts.dangerouslyForceUnsafeInstall) runtime.log(theme.warn(DEPRECATED_DANGEROUS_FORCE_UNSAFE_INSTALL_WARNING)); const acknowledgment = sourcePlan?.acknowledgement ?? (preflight.marketplace ? { sourceClass: "marketplace", spec: `${raw} from ${preflight.marketplace}` } : void 0); if (acknowledgment && !await confirmNonClawHubInstall({ acknowledged: opts.force, runtime, ...acknowledgment })) return runtime.exit(1); let result; try { const snapshot = await loadConfigForInstall(preflight.request).catch((error) => { if (error instanceof PluginInstallConfigError && error.blockedSnapshot) return error.blockedSnapshot; throw error; }); const gateway = params.applyRuntime || params.beforePersistentApply || hasPluginLifecycleLease() ? null : await resolvePluginLifecycleGateway(); const request = sourcePlan?.request ?? { source: "marketplace", marketplace: preflight.marketplace, plugin: raw, mode: preflight.installMode }; if (sourcePlan?.warning) runtime.log(theme.warn(sourcePlan.warning)); result = await installPluginWithHookFallback({ request: { ...request, ...opts.enable === false ? { enable: false } : {} }, snapshot, runtime, applyRuntime: params.applyRuntime, beforePersistentApply: params.beforePersistentApply, invalidateRuntimeCache: params.invalidateRuntimeCache ?? true, ...gateway ? { install: createGatewayPluginInstaller(gateway) } : {}, allowBundledFallback: sourcePlan?.allowBundledFallback, logger: createPluginInstallLogger(runtime), confirmInstall: resolveClawHubInstallConfirmation(), ...resolvePluginCapabilityConsentCliOptions({ acceptCapabilities: opts.acceptCapabilities, action: "install", runtime }), safetyOverrides: resolveInstallSafetyOverrides({ ...opts, config: snapshot.config, ...resolveInstallPolicyWarningAcknowledgementCliOptions({ acknowledgeInstallPolicyWarning: opts.acknowledgeInstallPolicyWarning, allowPrompt: params.allowInstallPolicyWarningPrompt }) }) }); } catch (error) { runtime.error(formatErrorMessage(error)); return runtime.exit(1); } if (!result.ok) { if (result.code !== CLAWHUB_INSTALL_ERROR_CODE.CLAWHUB_DOWNLOAD_BLOCKED || !result.warning?.trim()) runtime.error(result.error); return runtime.exit(1); } } //#endregion export { runPluginInstallCommand };