UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

445 lines (444 loc) 23.7 kB
import { o as asRecord } from "./record-coerce-DItp3I4t.js"; import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js"; import { y as uniqueStrings } from "./string-normalization-DsCfAx8q.js"; import { l as readConfigFileSnapshotForWrite } from "./io.runtime-B9iJRs3w.js"; import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js"; import { a as getProcessGatewayPluginMetadataSnapshot } from "./current-plugin-metadata-state-B1UoAr4G.js"; import { a as resolveDefaultPluginExtensionsDir } from "./install-paths-ChnxikBv.js"; import { t as parseClawHubPluginSpec } from "./clawhub-spec-Er3Np6VI.js"; import { I as CLAWHUB_INSTALL_ERROR_CODE, b as resolveOfficialExternalPluginInstallSources, o as getOfficialExternalPluginCatalogManifest, p as listOfficialExternalPluginCatalogEntries, v as resolveOfficialExternalPluginId, y as resolveOfficialExternalPluginInstall } from "./official-external-plugin-catalog-Dzu7dwBN.js"; import { r as loadInstalledPluginIndexInstallRecords } from "./installed-plugin-index-record-reader-SXWwf_BU.js"; import { n as resolvePluginControlPlaneWorkspace } from "./control-plane-workspace-BWhgt2gz.js"; import { t as createInstalledPluginOwnershipResolver } from "./installed-plugin-package-ownership-DGTVMT4h.js"; import { t as ensurePluginAllowlisted } from "./plugins-allowlist-DGbUrepm.js"; import { t as ManagedPluginLifecycleError } from "./management-lifecycle-error-BlJhejU6.js"; import { t as setPluginEnabledInConfig } from "./toggle-config-CVSe45eW.js"; import { t as enableExplicitlySelectedPluginInConfig } from "./enable-3OwjFbBV.js"; import { n as assertConfigWriteAllowedInCurrentMode } from "./nix-mode-write-guard-uwcnAyQN.js"; import { r as replaceConfigFile } from "./mutate-ZNN4iFCn.js"; import "./config-Cs0XXL3x.js"; import { t as ErrorCodes } from "./gateway-error-details-w0nAGBBp.js"; import { Fv as buildClawHubTrustErrorDetails, Gn as validatePluginsUninstallParams, Hn as validatePluginsSetEnabledParams, In as validatePluginsInstallParams, Iv as isClawHubTrustErrorCode, Mv as INSTALL_POLICY_WARNING_ACKNOWLEDGEMENT_REQUIRED, Nv as readInstallPolicyWarningErrorDetails } from "./src-BiL5aQto.js"; import { n as buildCapabilityConsentErrorDetails } from "./capability-consent-error-details-D4ZVP4Ge.js"; import { d as errorShape } from "./error-codes-Bo8q2D1o.js"; import { a as withPluginInstallRecords, o as withoutPluginInstallRecords, r as removePluginInstallRecordFromRecords } from "./installed-plugin-index-records-C06Xozeq.js"; import { t as createInstalledPluginIndexScopeLookup } from "./installed-plugin-index-scope-lookup-zFavZQ1h.js"; import { t as withPluginLifecycleLease } from "./plugin-lifecycle-lease-BtAFH872.js"; import { t as assertValidParams } from "./validation-pzrlzFvo.js"; import { a as resolvePluginCapabilityConsent } from "./capability-consent-DAQVy0Fy.js"; import { n as recordPluginPackageUninstallPlan, t as prepareConfigForDisabledPluginSet } from "./uninstall-package-plan-BcbmXONg.js"; import { a as planPluginUninstall, n as applyPluginUninstallDirectoryRemoval, o as pluginUninstallTargetExists, r as formatUninstallActionLabels } from "./uninstall-CTOrqce4.js"; import { i as commitPluginInstallRecordsWithConfig } from "./install-record-commit-C4pioTsf.js"; import { n as refreshPluginRegistryAfterConfigMutation } from "./registry-refresh-CvO7IUdQ.js"; import { t as applySlotSelectionForPlugin } from "./slot-selection-icx6WfxG.js"; import { i as selectInstallMutationWriteOptions, r as resolveInstallConfigMutationPreflights } from "./install-persistence-iNWG2xIJ.js"; import { t as installManagedPluginSource } from "./management-install-CPAJwGGP.js"; import { t as buildGatewayReloadPlan } from "./config-reload-plan-DUT3qlnP.js"; import { t as resolveGatewayReloadSettings } from "./config-reload-settings-q1wYjpRM.js"; import { t as collectChangedPaths } from "./config-change-paths-DODizdZf.js"; import { m as resolveOfficialEntryById, o as loadOfficialCatalog } from "./management-catalog-CoNziW12.js"; import { i as refreshManagedPluginMetadata, n as listManagedPlugins, r as loadFreshManagedPluginMetadata } from "./management-service-C_33JbZN.js"; import { t as collectClawPluginUninstallWarnings } from "./uninstall-claw-references-e8tvJIiQ.js"; //#region src/plugins/management-mutations.ts function assertValidConfigSnapshot(prepared) { const { snapshot, writeOptions } = prepared; if (!snapshot.valid) throw new ManagedPluginLifecycleError("Config invalid; run `openclaw doctor --fix` before managing plugins."); const mutationWriteOptions = selectInstallMutationWriteOptions(writeOptions); const { pluginMutation } = resolveInstallConfigMutationPreflights({ parsed: asRecord(snapshot.parsed), snapshotPath: snapshot.path, writeOptions: mutationWriteOptions }); if (pluginMutation.mode === "blocked") throw new ManagedPluginLifecycleError(pluginMutation.reason); return { config: snapshot.sourceConfig, baseHash: snapshot.hash, writeOptions: mutationWriteOptions }; } async function readPluginMutationSnapshot(env) { try { assertConfigWriteAllowedInCurrentMode({ env }); } catch (error) { throw new ManagedPluginLifecycleError(formatErrorMessage(error), { cause: error }); } return assertValidConfigSnapshot(await readConfigFileSnapshotForWrite()); } function createSilentRuntime() { return { log: () => void 0, error: () => void 0, exit: (code) => { throw new ManagedPluginLifecycleError(`plugin lifecycle exited with code ${code}`); } }; } function createInstallLogger(warnings) { return { info: () => void 0, warn: (message) => warnings.push(message) }; } /** Explicitly declared runtime id, ignoring the entry-id fallback used for display. */ function resolveDeclaredOfficialPluginId(entry) { const manifest = getOfficialExternalPluginCatalogManifest(entry); return normalizeOptionalString(manifest?.plugin?.id) ?? normalizeOptionalString(manifest?.channel?.id) ?? normalizeOptionalString(manifest?.providers?.[0]?.id); } function resolveOfficialEntryByClawHubPackage(entries, packageName) { return [...listOfficialExternalPluginCatalogEntries(), ...entries].find((entry) => { return resolveOfficialExternalPluginInstallSources(entry).some((source) => source.source === "clawhub" && parseClawHubPluginSpec(source.spec)?.name === packageName); }); } function resolveHostedOfficialEntryByClawHubPackage(entries, packageName) { return entries.find((entry) => { return resolveOfficialExternalPluginInstallSources(entry).some((source) => source.source === "clawhub" && parseClawHubPluginSpec(source.spec)?.name === packageName); }); } function buildClawHubSpec(packageName, version) { const parsed = parseClawHubPluginSpec(`clawhub:${packageName}`); if (!parsed || parsed.version) throw new ManagedPluginLifecycleError(`invalid ClawHub package name: ${packageName}`); return `clawhub:${packageName}${version ? `@${version}` : ""}`; } function throwInstallFailure(result) { const unavailable = !result.code || result.code === CLAWHUB_INSTALL_ERROR_CODE.ARTIFACT_UNAVAILABLE || result.code === CLAWHUB_INSTALL_ERROR_CODE.ARTIFACT_DOWNLOAD_UNAVAILABLE || result.code === CLAWHUB_INSTALL_ERROR_CODE.CLAWHUB_SECURITY_UNAVAILABLE; throw new ManagedPluginLifecycleError(result.error, { kind: unavailable ? "unavailable" : "invalid-request", code: result.code, version: result.version, warning: result.warning, installPolicyWarning: result.installPolicyWarning, cause: result }); } function resolveManagedClawHubInstallRequest(params) { const packageName = params.request.packageName.trim(); const official = resolveOfficialEntryByClawHubPackage(params.officialEntries, packageName); const expectedPluginId = official ? resolveDeclaredOfficialPluginId(official) : void 0; const hostedOfficial = resolveHostedOfficialEntryByClawHubPackage(params.officialEntries, packageName); const hostedSource = hostedOfficial ? resolveOfficialExternalPluginInstallSources(hostedOfficial).find((source) => source.source === "clawhub") : void 0; const hostedClawHub = parseClawHubPluginSpec(hostedSource?.spec ?? ""); const requestMatchesHostedCandidate = !params.request.version || params.request.version === hostedClawHub?.version; const version = params.request.version ?? (requestMatchesHostedCandidate ? hostedClawHub?.version : void 0); const expectedIntegrity = params.expectedIntegrity ?? (requestMatchesHostedCandidate ? hostedSource?.expectedIntegrity : void 0); return { source: "clawhub", spec: buildClawHubSpec(packageName, version), ...official ? { trustedSourceLinkedOfficialInstall: true } : {}, ...expectedPluginId ? { expectedPluginId } : {}, ...expectedIntegrity ? { expectedIntegrity } : {} }; } function resolveManagedOfficialInstallRequest(params) { const entry = resolveOfficialEntryById(params.officialEntries, params.request.pluginId); if (!entry) throw new ManagedPluginLifecycleError(`unknown official plugin catalog entry: ${params.request.pluginId}`); const pluginId = resolveOfficialExternalPluginId(entry); const install = resolveOfficialExternalPluginInstall(entry); if (!pluginId || !install) throw new ManagedPluginLifecycleError(`official plugin catalog entry is not installable: ${params.request.pluginId}`); const installSources = resolveOfficialExternalPluginInstallSources(entry); const primary = installSources[0]; if (!primary) throw new ManagedPluginLifecycleError(`official plugin catalog entry has no supported install source: ${params.request.pluginId}`); return { source: "official", spec: primary.spec, installSources, pluginId, expectedPluginId: resolveDeclaredOfficialPluginId(entry), mode: "install" }; } /** Install a ClawHub or curated official plugin through the canonical install pipeline. */ async function installManagedPlugin(params) { const env = params.env ?? process.env; return await withPluginLifecycleLease({ env }, async () => { const snapshot = await readPluginMutationSnapshot(env); const officialCatalog = await loadOfficialCatalog(); const warnings = []; const installLogger = createInstallLogger(warnings); const request = params.request.source === "clawhub" ? resolveManagedClawHubInstallRequest({ request: params.request, officialEntries: officialCatalog.entries }) : resolveManagedOfficialInstallRequest({ request: params.request, officialEntries: officialCatalog.entries }); const installed = await installManagedPluginSource({ request, snapshot, env, logger: installLogger, ...params.request.acknowledgeCapabilities ? { acknowledgeCapabilities: params.request.acknowledgeCapabilities } : {}, ...params.request.acknowledgeInstallPolicyWarning ? { safetyOverrides: { onInstallPolicyWarning: async () => ({ status: "approved" }) } } : {}, invalidateRuntimeCache: false, runtime: createSilentRuntime() }); if (!installed.ok) return throwInstallFailure(installed); warnings.push(...installed.warnings ?? []); const workspace = resolvePluginControlPlaneWorkspace({ config: installed.config, env }); if (workspace.diagnostic && !getProcessGatewayPluginMetadataSnapshot()) warnings.push(workspace.diagnostic.message); const installedMetadata = refreshManagedPluginMetadata({ config: installed.config, env }); const catalog = await listManagedPlugins({ config: installed.config, env, officialCatalog, metadata: installedMetadata }); const installedOwnership = createInstalledPluginOwnershipResolver(installedMetadata.index, env).resolvePackage(installed.pluginId); if (!installedOwnership.ok) throw new ManagedPluginLifecycleError(installedOwnership.error); const installedPluginIds = installedOwnership.value.pluginIds; const representativePluginId = installedPluginIds[0]; const plugin = catalog.plugins.find((entry) => entry.id === representativePluginId); if (!plugin) throw new ManagedPluginLifecycleError(`installed plugin missing from refreshed registry: ${installed.pluginId}`); return { plugin, ...installedPluginIds.length > 1 || warnings.length > 0 ? { warnings: [...installedPluginIds.length > 1 ? [`Installed package "${installed.pluginId}" with plugin entries: ${installedPluginIds.join(", ")}.`] : [], ...new Set(warnings)] } : {} }; }); } /** Persist desired plugin policy while preserving allow/deny, slot, include, and hash guards. */ async function setManagedPluginEnabled(params) { const env = params.env ?? process.env; return await withPluginLifecycleLease({ env }, async () => { const snapshot = await readPluginMutationSnapshot(env); const metadata = loadFreshManagedPluginMetadata(snapshot.config, env); const pluginId = metadata.normalizePluginId(params.pluginId.trim()); const installedPlugin = metadata.index.plugins.find((plugin) => plugin.pluginId === pluginId); if (!installedPlugin) throw new ManagedPluginLifecycleError(`plugin not installed: ${params.pluginId}`); if (params.enabled && !installedPlugin.enabled) await resolvePluginCapabilityConsent({ config: snapshot.config, env, pluginId, acknowledge: params.acknowledgeCapabilities, metadata }); let next = snapshot.config; const warnings = []; let policyPluginId = pluginId; if (params.enabled) { if ((next.plugins?.allow?.length ?? 0) > 0) next = ensurePluginAllowlisted(next, pluginId); const enableResult = enableExplicitlySelectedPluginInConfig(next, pluginId, { updateChannelConfig: false }); if (!enableResult.enabled) throw new ManagedPluginLifecycleError(`plugin "${pluginId}" could not be enabled (${enableResult.reason ?? "unknown reason"})`); next = enableResult.config; policyPluginId = enableResult.pluginId; const slotResult = applySlotSelectionForPlugin(next, pluginId, metadata); next = slotResult.config; warnings.push(...slotResult.warnings); } else next = setPluginEnabledInConfig(next, pluginId, false, { updateChannelConfig: false }); const changedPaths = /* @__PURE__ */ new Set(); collectChangedPaths(snapshot.config, next, "", changedPaths); await replaceConfigFile({ nextConfig: next, baseHash: snapshot.baseHash, writeOptions: snapshot.writeOptions }); await refreshPluginRegistryAfterConfigMutation({ config: next, env, reason: "policy-changed", invalidateRuntimeCache: false, policyPluginIds: [policyPluginId], logger: { warn: (message) => warnings.push(message) } }); const updatedMetadata = refreshManagedPluginMetadata({ config: next, env }); const plugin = (await listManagedPlugins({ config: next, env, metadata: updatedMetadata })).plugins.find((entry) => entry.id === pluginId); if (!plugin) throw new ManagedPluginLifecycleError(`updated plugin missing from refreshed registry: ${pluginId}`); return { plugin, changedPaths: [...changedPaths].filter(Boolean).toSorted(), ...warnings.length > 0 ? { warnings } : {} }; }); } /** Remove an installed plugin: config references, install record, and managed files. */ async function uninstallManagedPlugin(params) { const env = params.env ?? process.env; return await withPluginLifecycleLease({ env }, async () => { const snapshot = await readPluginMutationSnapshot(env); const installRecords = await loadInstalledPluginIndexInstallRecords({ env }); const configWithRecords = withPluginInstallRecords(snapshot.config, installRecords); const metadata = loadFreshManagedPluginMetadata(configWithRecords, env); const pluginId = metadata.normalizePluginId(params.pluginId.trim()); const record = metadata.index.plugins.find((plugin) => plugin.pluginId === pluginId); if (record?.origin === "bundled") throw new ManagedPluginLifecycleError(`bundled plugin cannot be uninstalled: ${pluginId}; disable it instead`); if (!record && !Object.hasOwn(installRecords, pluginId)) throw new ManagedPluginLifecycleError(`Plugin not found: ${pluginId}`); const ownership = createInstalledPluginOwnershipResolver(metadata.index, env).resolveLifecycle(pluginId); if (!ownership.ok) throw new ManagedPluginLifecycleError(ownership.error); const { installOwner, pluginIds: ownedPluginIds } = ownership.value; const policyPluginIds = ownedPluginIds.length > 0 ? ownedPluginIds : [installOwner]; const ownedManifests = ownedPluginIds.flatMap((entryId) => { const manifest = metadata.byPluginId.get(entryId); return manifest ? [manifest] : []; }); const channelIds = ownedManifests.length > 0 ? uniqueStrings(ownedManifests.flatMap((manifest) => manifest.channels)) : ownership.value.kind === "orphan" && createInstalledPluginIndexScopeLookup(metadata.index).hasChannelContributionOwners([installOwner]) ? [] : void 0; const extensionsDir = resolveDefaultPluginExtensionsDir(env); const initialPlan = planPluginUninstall(recordPluginPackageUninstallPlan({ config: configWithRecords, pluginId: installOwner, ...channelIds !== void 0 ? { channelIds } : {}, deleteFiles: true, extensionsDir }, { runtimePluginIds: policyPluginIds, runtimeLoadPaths: ownedPluginIds.flatMap((entryId) => metadata.byPluginId.get(entryId)?.source ?? []) })); if (!initialPlan.ok) throw new ManagedPluginLifecycleError(initialPlan.error); let plan = initialPlan; let finalSnapshot = snapshot; let directoryResult = { directoryRemoved: false, warnings: [] }; if (plan.directoryRemoval) { const disabledConfig = prepareConfigForDisabledPluginSet(snapshot.config, policyPluginIds); await replaceConfigFile({ nextConfig: disabledConfig, baseHash: snapshot.baseHash, writeOptions: { ...snapshot.writeOptions, afterWrite: { mode: "auto" } } }); directoryResult = await applyPluginUninstallDirectoryRemoval(plan.directoryRemoval); if (pluginUninstallTargetExists(plan.directoryRemoval.target)) throw new ManagedPluginLifecycleError(`Failed to remove plugin directory ${plan.directoryRemoval.target}; the plugin remains disabled and tracked so uninstall can be retried.`, { kind: "unavailable" }); finalSnapshot = await readPluginMutationSnapshot(env); const refreshedConfigWithRecords = withPluginInstallRecords(finalSnapshot.config, installRecords); const refreshedPlan = planPluginUninstall(recordPluginPackageUninstallPlan({ config: refreshedConfigWithRecords, pluginId: installOwner, ...channelIds !== void 0 ? { channelIds } : {}, deleteFiles: true, extensionsDir }, { runtimePluginIds: policyPluginIds, runtimeLoadPaths: ownedPluginIds.flatMap((entryId) => metadata.byPluginId.get(entryId)?.source ?? []) })); if (!refreshedPlan.ok) throw new ManagedPluginLifecycleError(refreshedPlan.error); plan = refreshedPlan; } const nextConfig = withoutPluginInstallRecords(plan.config); const nextInstallRecords = removePluginInstallRecordFromRecords(installRecords, installOwner); await commitPluginInstallRecordsWithConfig({ previousInstallRecords: installRecords, nextInstallRecords, nextConfig, baseHash: finalSnapshot.baseHash, writeOptions: finalSnapshot.writeOptions }); const warnings = [ ...collectClawPluginUninstallWarnings({ pluginId: installOwner, installRecord: installRecords[installOwner], env }), ...pluginId !== installOwner || ownedPluginIds.length > 1 ? [`Uninstalled package "${installOwner}" and all owned plugin entries: ${ownedPluginIds.join(", ")}.`] : [], ...directoryResult.warnings ]; await refreshPluginRegistryAfterConfigMutation({ config: nextConfig, env, reason: "source-changed", installRecords: nextInstallRecords, invalidateRuntimeCache: false, logger: { warn: (message) => warnings.push(message) } }); refreshManagedPluginMetadata({ config: nextConfig, env }); return { pluginId: installOwner, removed: formatUninstallActionLabels({ ...plan.actions, directory: directoryResult.directoryRemoved }), ...warnings.length > 0 ? { warnings: [...new Set(warnings)] } : {} }; }); } //#endregion //#region src/gateway/server-methods/plugins-mutations.ts function pluginPolicyRestartRequired(params) { const plan = buildGatewayReloadPlan([...params.changedPaths]); const mode = resolveGatewayReloadSettings(params.config).mode; return plan.restartGateway || mode === "off"; } const pluginMutationHandlers = { "plugins.install": async ({ params, respond }) => { if (!assertValidParams(params, validatePluginsInstallParams, "plugins.install", respond)) return; try { const result = await installManagedPlugin({ request: params }); respond(true, { ok: true, plugin: result.plugin, restartRequired: true, ...result.warnings ? { warnings: result.warnings } : {} }, void 0); } catch (error) { const lifecycleError = error instanceof ManagedPluginLifecycleError ? error : void 0; const trustCode = lifecycleError?.code && isClawHubTrustErrorCode(lifecycleError.code) ? lifecycleError.code : void 0; const trustDetails = lifecycleError ? buildClawHubTrustErrorDetails({ ...trustCode ? { code: trustCode } : {}, ...lifecycleError.version ? { version: lifecycleError.version } : {}, ...lifecycleError.warning ? { warning: lifecycleError.warning } : {} }) : void 0; const installPolicyDetails = lifecycleError?.installPolicyWarning ? readInstallPolicyWarningErrorDetails({ installPolicyCode: INSTALL_POLICY_WARNING_ACKNOWLEDGEMENT_REQUIRED, ...lifecycleError.installPolicyWarning }) : void 0; const details = (lifecycleError?.capabilityConsent ? buildCapabilityConsentErrorDetails(lifecycleError.capabilityConsent) : void 0) ?? installPolicyDetails ?? trustDetails; respond(false, void 0, errorShape(lifecycleError?.kind === "invalid-request" ? ErrorCodes.INVALID_REQUEST : ErrorCodes.UNAVAILABLE, formatErrorMessage(error), details ? { details } : void 0)); } }, "plugins.uninstall": async ({ params, respond }) => { if (!assertValidParams(params, validatePluginsUninstallParams, "plugins.uninstall", respond)) return; try { const result = await uninstallManagedPlugin({ pluginId: params.pluginId }); respond(true, { ok: true, pluginId: result.pluginId, restartRequired: true, removed: result.removed, ...result.warnings ? { warnings: result.warnings } : {} }, void 0); } catch (error) { const lifecycleError = error instanceof ManagedPluginLifecycleError ? error : void 0; respond(false, void 0, errorShape(lifecycleError?.kind === "invalid-request" ? ErrorCodes.INVALID_REQUEST : ErrorCodes.UNAVAILABLE, formatErrorMessage(error))); } }, "plugins.setEnabled": async ({ params, respond, context }) => { if (!assertValidParams(params, validatePluginsSetEnabledParams, "plugins.setEnabled", respond)) return; try { const result = await setManagedPluginEnabled({ pluginId: params.pluginId, enabled: params.enabled, ...params.acknowledgeCapabilities ? { acknowledgeCapabilities: params.acknowledgeCapabilities } : {} }); respond(true, { ok: true, plugin: result.plugin, restartRequired: pluginPolicyRestartRequired({ config: context.getRuntimeConfig(), changedPaths: result.changedPaths }), ...result.warnings ? { warnings: result.warnings } : {} }, void 0); } catch (error) { const lifecycleError = error instanceof ManagedPluginLifecycleError ? error : void 0; respond(false, void 0, errorShape(lifecycleError?.kind === "invalid-request" ? ErrorCodes.INVALID_REQUEST : ErrorCodes.UNAVAILABLE, formatErrorMessage(error), lifecycleError?.capabilityConsent ? { details: buildCapabilityConsentErrorDetails(lifecycleError.capabilityConsent) } : void 0)); } } }; //#endregion export { pluginMutationHandlers };