openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
419 lines (418 loc) • 18 kB
JavaScript
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 };