UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

419 lines (418 loc) 18 kB
import { c as isRecord } from "./record-coerce-DItp3I4t.js"; import { y as uniqueStrings } from "./string-normalization-DsCfAx8q.js"; import { n as validateJsonSchemaValue } from "./schema-validator-CLfGeb79.js"; import { t as VERSION } from "./version-v1kuAkGj.js"; import { o as parseRegistryNpmSpec } from "./npm-registry-spec-CM_p1_uq.js"; import { a as resolveDefaultPluginExtensionsDir } from "./install-paths-ChnxikBv.js"; import { t as parseClawHubPluginSpec } from "./clawhub-spec-Er3Np6VI.js"; import { A as installWithSourceFallback, L as isUnavailableClawHubTarget, M as resolveClawHubInstallSpecsForUpdateChannel, P as resolveNpmInstallSpecsForUpdateChannel, a as getOfficialExternalPluginCatalogEntryForPackage } from "./official-external-plugin-catalog-Dzu7dwBN.js"; import { c as normalizeUpdateChannel, d as resolveRegistryUpdateChannel } from "./update-channels-BcfztK6k.js"; import { n as isUnavailableNpmTarget, t as PLUGIN_INSTALL_ERROR_CODE } from "./install-types-DY_kphq4.js"; import { t as buildNpmResolutionFields } from "./install-source-utils-DPJygB4h.js"; import { t as withPluginLifecycleLease } from "./plugin-lifecycle-lease-BtAFH872.js"; import { r as prepareManagedPluginArtifactConsentHandler } from "./capability-consent-DAQVy0Fy.js"; import { B as requestDeferredPluginInstall, H as resolvePluginInstallTransaction } from "./install-managed-npm-state-BlNS_UI7.js"; import { t as persistPluginInstall } from "./install-persistence-iNWG2xIJ.js"; import { i as installPluginFromNpmPackArchive, n as installPluginFromPath, r as installPluginFromNpmSpec } from "./install-BezM8CtS.js"; import { t as installPluginFromGitSpec } from "./git-install-BNE75LQq.js"; import { t as buildClawHubPluginInstallRecordFields } from "./clawhub-install-records-Dy2deHKG.js"; import { t as installPluginFromClawHub } from "./clawhub-DgZbJqg3.js"; import { t as installPluginFromMarketplace } from "./marketplace-ClbNhrdY.js"; //#region src/plugins/bundled-install.ts function resolveBundledPluginConfigEnablement(params) { if (!params.bundledSource.requiresConfig) return { mode: "ready" }; const entry = isRecord(params.existingEntry) ? params.existingEntry : void 0; if (!entry || !Object.hasOwn(entry, "config")) return { mode: "missing" }; const config = entry.config; if (!params.bundledSource.configSchema) return isRecord(config) && Object.keys(config).length > 0 ? { mode: "ready" } : { mode: "invalid", error: "config must be a non-empty object" }; const result = validateJsonSchemaValue({ schema: params.bundledSource.configSchema, cacheKey: `bundled-install:${params.bundledSource.pluginId}`, value: config, applyDefaults: true }); return result.ok ? { mode: "ready" } : { mode: "invalid", error: result.errors[0]?.text ?? "invalid plugin config" }; } function prepareConfigForDisabledBundledInstall(config, pluginId) { const entry = config.plugins?.entries?.[pluginId]; const policy = isRecord(entry) ? { ...entry } : {}; delete policy.config; return { ...config, plugins: { ...config.plugins, entries: { ...config.plugins?.entries, [pluginId]: { ...policy, enabled: false } } } }; } async function installBundledPluginSource(params) { const existingEntry = params.snapshot.config.plugins?.entries?.[params.bundledSource.pluginId]; const configEnablement = resolveBundledPluginConfigEnablement({ bundledSource: params.bundledSource, existingEntry }); if (configEnablement.mode === "invalid") throw new Error(`Plugin "${params.bundledSource.pluginId}" has invalid configured settings: ${configEnablement.error}. Fix plugins.entries.${params.bundledSource.pluginId}.config, then rerun the install.`); const shouldEnable = configEnablement.mode === "ready"; const configBase = shouldEnable ? params.snapshot.config : prepareConfigForDisabledBundledInstall(params.snapshot.config, params.bundledSource.pluginId); const configWarning = shouldEnable ? void 0 : `Installed bundled plugin "${params.bundledSource.pluginId}" without enabling it because it requires configuration first. Configure it, then run \`openclaw plugins enable ${params.bundledSource.pluginId}\`.`; const warnings = [params.warning, configWarning].filter((warning) => Boolean(warning)); await persistPluginInstall({ ...params, snapshot: { ...params.snapshot, config: configBase }, pluginId: params.bundledSource.pluginId, install: { source: "path", spec: params.rawSpec, sourcePath: params.bundledSource.localPath, installPath: params.bundledSource.localPath }, enable: shouldEnable, ...warnings.length > 0 ? { warningMessage: warnings.join("\n") } : {} }); return { pluginId: params.bundledSource.pluginId, warnings }; } //#endregion //#region src/plugins/management-install.ts async function persistManagedSourceInstall(params) { const warnings = []; let committed = false; try { return { config: await persistPluginInstall({ snapshot: params.snapshot, pluginId: params.pluginId, install: params.install, invalidateRuntimeCache: params.invalidateRuntimeCache, runtime: params.runtime, persistenceLogger: { warn: (message) => warnings.push(message) }, beforePersistentApply: params.beforePersistentApply, beforePersistentEffect: params.beforePersistentEffect, onCommitted: () => { committed = true; }, ...params.successMessage ? { successMessage: params.successMessage } : {} }), warnings }; } catch (error) { if (!committed) try { await params.transaction?.rollback(); } catch (rollbackError) { const aggregate = new AggregateError([error, rollbackError], "Plugin install failed and payload rollback failed"); aggregate.cause = error; throw aggregate; } throw error; } finally { if (committed) await params.transaction?.commit().catch(() => { const warning = "Plugin install committed, but backup cleanup failed. Restart is required."; warnings.push(warning); params.runtime?.log(warning); }); } } /** * Official plugin installs target the release stream the gateway is running, * the same target `openclaw doctor --fix` and `openclaw plugins update` * already resolve. Resolving here keeps every managed install path — CLI, * chat command, and any future caller — on one answer instead of letting the * registry default land a plugin the gateway then reports as drifted. * * Beta and extended-stable resolve here. Version-bound stable tracks key off a * per-plugin `versionBoundToOpenClaw` descriptor that a managed install request * does not carry, and answering for them from this boundary would pin plugins * the policy never opted in. */ function resolveOfficialManagedInstallSpec(params) { const { request } = params; const trustedSourceLinkedOfficialInstall = request.source !== "official" && request.trustedSourceLinkedOfficialInstall === true; if (request.source === "npm" && !trustedSourceLinkedOfficialInstall) return null; if (request.expectedIntegrity) return null; const packageName = request.source === "clawhub" ? parseClawHubPluginSpec(request.spec)?.name : parseRegistryNpmSpec(request.spec)?.name; if (!packageName || request.source !== "official" && !trustedSourceLinkedOfficialInstall && !getOfficialExternalPluginCatalogEntryForPackage(packageName)) return null; const updateChannel = resolveRegistryUpdateChannel({ configChannel: normalizeUpdateChannel(params.config.update?.channel), currentVersion: VERSION }); if (updateChannel !== "beta" && updateChannel !== "extended-stable") return null; const specs = request.source === "clawhub" ? resolveClawHubInstallSpecsForUpdateChannel({ spec: request.spec, updateChannel, officialPackageName: packageName, coreVersion: VERSION }) : resolveNpmInstallSpecsForUpdateChannel({ spec: request.spec, updateChannel, officialPackageName: packageName, coreVersion: VERSION }); return specs.installSpec === request.spec ? null : specs.installSpec; } /** * Installs official plugins from the release stream the gateway runs. When that * stream has no published artifact the install reports it instead of widening * back to the registry default: widening would resolve `latest` and land exactly * the cross-release plugin this boundary exists to prevent, and a fresh install * has nothing to preserve, so failing with the reason costs the operator only a * retry with an explicit version. */ async function installManagedPluginSource(params) { return await withPluginLifecycleLease({ env: params.env }, async (lease) => { const assertOwned = lease.assertOwned.bind(lease); return await installManagedPluginSourceUnderLease({ ...params, beforePersistentApply: () => { params.beforePersistentApply?.(); assertOwned(); } }, assertOwned); }); } async function installManagedPluginSourceUnderLease(params, assertOwned) { const { request } = params; if (request.source === "official" && request.installSources) { const { attempt: installAttempt, source: installedSource } = await installWithSourceFallback({ sources: request.pin ? request.installSources.filter((source) => source.source === "npm") : request.installSources, install: async (source) => await installManagedPluginSource({ ...params, request: { source: source.source, spec: source.spec, mode: request.mode, expectedPluginId: request.expectedPluginId, trustedSourceLinkedOfficialInstall: true, ...source.expectedIntegrity ? { expectedIntegrity: source.expectedIntegrity } : {}, ...source.source === "npm" && request.pin ? { pin: true } : {} } }), result: (attempt) => attempt, onFallback: (message) => params.logger?.warn?.(message) }); return installAttempt.ok ? installAttempt : { ...installAttempt, installSource: installedSource }; } if (request.source !== "official" && request.source !== "npm" && request.source !== "clawhub") return await installResolvedManagedPluginSource(params, assertOwned); const installSpec = resolveOfficialManagedInstallSpec({ request, config: params.snapshot.config }); if (!installSpec) return await installResolvedManagedPluginSource(params, assertOwned); const result = await installResolvedManagedPluginSource({ ...params, request: { ...request, spec: installSpec, recordSpec: request.recordSpec ?? request.spec } }, assertOwned); if (result.ok) return result; if (!(request.source === "clawhub" ? isUnavailableClawHubTarget(result) : isUnavailableNpmTarget(result))) return result; return { ...result, code: PLUGIN_INSTALL_ERROR_CODE.RELEASE_COHORT_UNAVAILABLE, error: `No ${installSpec} release is published for this gateway. Installing ${request.spec} would resolve a build from another release; pass an explicit version to install one anyway.` }; } /** Execute one resolved plugin source through the shared install-and-persist pipeline. */ async function installResolvedManagedPluginSource(params, assertOwned) { const { request } = params; const env = params.env ?? process.env; const extensionsDir = resolveDefaultPluginExtensionsDir(env); if (request.source === "bundled") return { ok: true, ...await installBundledPluginSource({ ...params, rawSpec: request.rawSpec, bundledSource: request.bundledSource, warning: request.warning }), config: params.snapshot.config }; const consentExemptSource = request.source === "local" && request.bundledOrigin === true; const source = request.source === "local" ? request.recordSource : request.source === "npm-pack" || request.source === "official" ? "npm" : request.source; const capabilityConsent = consentExemptSource ? void 0 : await prepareManagedPluginArtifactConsentHandler({ config: params.snapshot.config, env, source, ...request.source === "marketplace" ? { spec: `${request.plugin}@${request.marketplace}` } : "spec" in request ? { spec: request.spec } : {}, ..."expectedIntegrity" in request && request.expectedIntegrity ? { expectedIntegrity: request.expectedIntegrity } : {}, acknowledgeCapabilities: params.acknowledgeCapabilities, onCapabilityConsent: params.onCapabilityConsent }); const common = requestDeferredPluginInstall({ ...params.safetyOverrides, config: params.snapshot.config, extensionsDir, logger: params.logger, beforePersistentApply: params.beforePersistentApply, ...capabilityConsent || params.beforePersistentEffect ? { onBeforePluginArtifactCommit: async (artifact) => { await capabilityConsent?.onBeforePluginArtifactCommit(artifact); await params.beforePersistentEffect?.(); } } : {} }, void 0, assertOwned); const complete = async (installResult, completed) => { const result = await installResult; if (!result.ok) return result; const installed = result; if (request.source === "local" && request.link) await capabilityConsent?.onBeforePluginArtifactCommit({ pluginId: installed.pluginId, stagedArtifactDir: request.path, mode: request.mode ?? "install" }); const transaction = resolvePluginInstallTransaction(installed); if (completed.expectedPluginId && installed.pluginId !== completed.expectedPluginId) { await transaction?.rollback(); return { ok: false, error: `official catalog plugin id mismatch: expected ${completed.expectedPluginId}, got ${installed.pluginId}` }; } const persisted = await persistManagedSourceInstall({ ...params, snapshot: completed.snapshot ?? params.snapshot, pluginId: installed.pluginId, install: capabilityConsent ? capabilityConsent.applyAcceptedSurface(installed.pluginId, completed.install(installed)) : completed.install(installed), transaction, successMessage: completed.successMessage, beforePersistentApply: params.beforePersistentApply }); return { ...installed, config: persisted.config, ...persisted.warnings.length > 0 ? { warnings: [...new Set(persisted.warnings)] } : {} }; }; if (request.source === "local") { const installPath = request.link ? request.path : void 0; const linkedSnapshot = request.link ? { ...params.snapshot, config: { ...params.snapshot.config, plugins: { ...params.snapshot.config.plugins, load: { ...params.snapshot.config.plugins?.load, paths: uniqueStrings([...params.snapshot.config.plugins?.load?.paths ?? [], request.path]) } } } } : params.snapshot; return await complete(installPluginFromPath({ ...common, path: request.path, mode: request.mode, ...request.link ? { dryRun: true, allowSourceTypeScriptEntries: true } : {} }), { snapshot: linkedSnapshot, successMessage: request.successMessage, install: (result) => ({ source: request.recordSource, sourcePath: request.recordPath ?? request.path, installPath: installPath ?? result.targetDir, version: result.version }) }); } if (request.source === "marketplace") return await complete(installPluginFromMarketplace({ ...common, marketplace: request.marketplace, plugin: request.plugin, mode: request.mode }), { install: (result) => ({ source: "marketplace", installPath: result.targetDir, version: result.version, marketplaceName: result.marketplaceName, marketplaceSource: result.marketplaceSource, marketplacePlugin: result.marketplacePlugin }) }); if (request.source === "npm-pack") return await complete(installPluginFromNpmPackArchive({ ...common, archivePath: request.archivePath, mode: request.mode }), { install: (result) => ({ source: "npm", spec: result.npmResolution?.resolvedSpec ?? result.manifestName ?? result.pluginId, sourcePath: request.archivePath, installPath: result.targetDir, ...result.version ? { version: result.version } : {}, ...buildNpmResolutionFields(result.npmResolution), artifactKind: "npm-pack", artifactFormat: "tgz", ...result.npmResolution?.integrity ? { npmIntegrity: result.npmResolution.integrity } : {}, ...result.npmResolution?.shasum ? { npmShasum: result.npmResolution.shasum } : {}, ...result.npmTarballName ? { npmTarballName: result.npmTarballName } : {} }) }); if (request.source === "git") return await complete(installPluginFromGitSpec({ ...common, spec: request.spec, mode: request.mode }), { install: (result) => ({ source: "git", spec: request.spec, installPath: result.targetDir, version: result.version, resolvedAt: result.git.resolvedAt, gitUrl: result.git.url, gitRef: result.git.ref, gitCommit: result.git.commit }) }); if (request.source === "clawhub") return await complete(installPluginFromClawHub({ ...common, spec: request.spec, mode: request.mode, ...request.expectedPluginId ? { expectedPluginId: request.expectedPluginId } : {}, ...request.expectedIntegrity ? { expectedIntegrity: request.expectedIntegrity } : {}, ...request.confirmInstall ? { confirmInstall: request.confirmInstall } : {} }), { expectedPluginId: request.expectedPluginId, install: (result) => ({ ...buildClawHubPluginInstallRecordFields(result.clawhub), spec: request.recordSpec ?? request.spec, installPath: result.targetDir }) }); const expectedPluginId = request.source === "official" ? request.pluginId : request.expectedPluginId; return await complete(installPluginFromNpmSpec({ ...common, spec: request.spec, mode: request.mode, ...request.source === "official" || request.trustedSourceLinkedOfficialInstall ? { trustedSourceLinkedOfficialInstall: true } : {}, ...expectedPluginId ? { expectedPluginId } : {}, ...request.expectedIntegrity ? { expectedIntegrity: request.expectedIntegrity } : {} }), { expectedPluginId, install: (result) => ({ source: "npm", spec: request.pin ? result.npmResolution?.resolvedSpec ?? request.spec : request.recordSpec ?? request.spec, installPath: result.targetDir, ...result.version ? { version: result.version } : {}, ...buildNpmResolutionFields(result.npmResolution) }) }); } //#endregion export { installManagedPluginSource as t };