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