mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-29 22:43:34 +00:00
Surface active official external plugin version drift in gateway status diagnostics so users can see when a host/package update left npm or ClawHub plugins behind the running local gateway. The advisory uses the daemon service install records, compares against the running gateway version, gives detailed fix commands in deep status, and avoids local-state drift checks for remote gateway mode or explicit status probe URLs. Co-authored-by: Hussein Nourelddine <hussein@gptc.com.kw>
128 lines
4.1 KiB
TypeScript
128 lines
4.1 KiB
TypeScript
import type { OpenClawConfig } from "../config/types.js";
|
|
import type { PluginInstallRecord } from "../config/types.plugins.js";
|
|
import { parseClawHubPluginSpec } from "../infra/clawhub-spec.js";
|
|
import { parseRegistryNpmSpec } from "../infra/npm-registry-spec.js";
|
|
import { normalizePluginsConfig, resolveEffectiveEnableState } from "./config-state.js";
|
|
import {
|
|
resolveTrustedSourceLinkedOfficialClawHubInstall,
|
|
resolveTrustedSourceLinkedOfficialNpmSpec,
|
|
} from "./official-external-install-records.js";
|
|
|
|
export type PluginVersionDriftEntry = {
|
|
pluginId: string;
|
|
installedVersion: string;
|
|
gatewayVersion: string;
|
|
source: PluginInstallRecord["source"];
|
|
packageName?: string;
|
|
spec?: string;
|
|
};
|
|
|
|
export type PluginVersionDriftReport = {
|
|
gatewayVersion: string;
|
|
drifts: PluginVersionDriftEntry[];
|
|
};
|
|
|
|
/**
|
|
* Strip a trailing build qualifier (e.g. `2026.5.4-1` -> `2026.5.4`) so that
|
|
* a gateway packaged as `2026.5.4-1` is not reported as drifted from a
|
|
* plugin packaged as `2026.5.4`. Both ends are normalized identically.
|
|
*/
|
|
function normalizeVersion(value: string): string {
|
|
return value.replace(/-\d+$/, "");
|
|
}
|
|
|
|
function isPluginEnabled(config: OpenClawConfig | undefined, pluginId: string): boolean {
|
|
const normalizedPluginConfig = normalizePluginsConfig(config?.plugins);
|
|
return resolveEffectiveEnableState({
|
|
id: pluginId,
|
|
origin: "global",
|
|
config: normalizedPluginConfig,
|
|
rootConfig: config,
|
|
}).enabled;
|
|
}
|
|
|
|
function shouldCompareOfficialInstallToGateway(params: {
|
|
pluginId: string;
|
|
record: PluginInstallRecord;
|
|
}): boolean {
|
|
const officialNpmSpec = resolveTrustedSourceLinkedOfficialNpmSpec(params);
|
|
if (officialNpmSpec) {
|
|
return parseRegistryNpmSpec(officialNpmSpec)?.selectorKind !== "exact-version";
|
|
}
|
|
const officialClawHubInstall = resolveTrustedSourceLinkedOfficialClawHubInstall(params);
|
|
if (officialClawHubInstall) {
|
|
if (officialClawHubInstall.clawhubSpec) {
|
|
return !parseClawHubPluginSpec(officialClawHubInstall.clawhubSpec)?.version;
|
|
}
|
|
return (
|
|
parseRegistryNpmSpec(officialClawHubInstall.npmSpec ?? "")?.selectorKind !== "exact-version"
|
|
);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Compare active official external plugin installs against the running gateway
|
|
* version and return any mismatches.
|
|
*
|
|
* @param params.gatewayVersion The gateway version string (typically the
|
|
* `version` field of the installed openclaw package.json).
|
|
* @param params.installRecords The full set of recorded plugin installs (as
|
|
* produced by `loadInstalledPluginIndexInstallRecords`).
|
|
* @param params.config The merged daemon-side OpenClawConfig (optional).
|
|
* Plugins inactive under the effective activation policy are skipped.
|
|
*
|
|
* The returned `drifts` list is sorted by `pluginId` for stable output.
|
|
*/
|
|
export function detectPluginVersionDrift(params: {
|
|
gatewayVersion: string;
|
|
installRecords: Record<string, PluginInstallRecord>;
|
|
config?: OpenClawConfig;
|
|
}): PluginVersionDriftReport {
|
|
const { gatewayVersion, installRecords, config } = params;
|
|
const normalizedGateway = normalizeVersion(gatewayVersion);
|
|
const drifts: PluginVersionDriftEntry[] = [];
|
|
|
|
for (const [pluginId, record] of Object.entries(installRecords)) {
|
|
if (!record) {
|
|
continue;
|
|
}
|
|
if (!isPluginEnabled(config, pluginId)) {
|
|
continue;
|
|
}
|
|
if (
|
|
!shouldCompareOfficialInstallToGateway({
|
|
pluginId,
|
|
record,
|
|
})
|
|
) {
|
|
continue;
|
|
}
|
|
const installedVersion = record.resolvedVersion ?? record.version;
|
|
if (!installedVersion) {
|
|
// No version recorded for this install — nothing to compare against.
|
|
// Don't fabricate drift; surface tooling (status.print) can flag this
|
|
// separately if desired.
|
|
continue;
|
|
}
|
|
if (normalizeVersion(installedVersion) === normalizedGateway) {
|
|
continue;
|
|
}
|
|
drifts.push({
|
|
pluginId,
|
|
installedVersion,
|
|
gatewayVersion,
|
|
source: record.source,
|
|
...(record.resolvedName ? { packageName: record.resolvedName } : {}),
|
|
...(record.spec ? { spec: record.spec } : {}),
|
|
});
|
|
}
|
|
|
|
drifts.sort((a, b) => a.pluginId.localeCompare(b.pluginId));
|
|
|
|
return {
|
|
gatewayVersion,
|
|
drifts,
|
|
};
|
|
}
|