Files
openclaw/src/plugins/plugin-version-drift.ts
Peter Steinberger 98e3f729bc refactor: remove dead plugin loader exports (#105937)
* refactor(plugins): trim activation and contract exports

* test(plugins): restore fixture cleanup

* refactor(plugins): trim install and loader exports

* test(plugins): fully reset loader caches

* refactor(plugins): trim metadata and catalog exports

* test(plugins): preserve catalog trust coverage

* refactor(plugins): trim provider and plugin exports

* refactor(plugins): trim runtime and tool exports

* test(plugins): update dead-export consumers

* test(plugins): remove empty dead-export suites

* refactor(plugins): align exports with split registry

* refactor(plugins): trim drifted loader exports

* style(plugins): format test fixtures

* refactor(scripts): use supported plugin APIs

* refactor(plugins): finish dead export cleanup

* chore(deadcode): refresh export baseline

* test(cli): mock production memory state

* chore(deadcode): sync latest export baseline

* fix(tests): keep plugin fixtures inside core

* chore(deadcode): refresh rebased export baseline

* chore(deadcode): sync current ratchets

* fix(plugins): retain reserved slot invariant

* fix(plugins): preserve dead-export invariants

* test(plugins): use neutral catalog query fixture

* test(plugins): satisfy catalog lint

* test(plugins): preserve integrity drift coverage

* fix(ci): register skill experience live proof
2026-07-13 01:29:33 -07:00

152 lines
5.1 KiB
TypeScript

// Detects plugin version drift between config, manifests, and installs.
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";
type PluginVersionDriftEntry = {
pluginId: string;
installedVersion: string;
gatewayVersion: string;
source: PluginInstallRecord["source"];
packageName?: string;
spec?: string;
};
export type PluginVersionDriftReport = {
gatewayVersion: string;
drifts: PluginVersionDriftEntry[];
};
function resolveExactNpmPinPackageName(entry: PluginVersionDriftEntry): string | undefined {
if (entry.source !== "npm" || !entry.spec) {
return undefined;
}
const parsed = parseRegistryNpmSpec(entry.spec);
if (parsed?.selectorKind !== "exact-version") {
return undefined;
}
return parsed.name;
}
/** Exact npm pins need a package@version target; id-only updates preserve the old pin. */
export function resolvePluginVersionDriftUpdateCommand(entry: PluginVersionDriftEntry): string {
const exactNpmPackageName = resolveExactNpmPinPackageName(entry);
if (exactNpmPackageName) {
const exactNpmTarget = `${exactNpmPackageName}@${entry.gatewayVersion}`;
if (parseRegistryNpmSpec(exactNpmTarget)?.selectorKind === "exact-version") {
return `openclaw plugins update ${exactNpmTarget}`;
}
}
return `openclaw plugins update ${entry.pluginId}`;
}
/**
* 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,
};
}