fix(plugins): sync official plugin installs during update (#78065)

* fix(plugins): sync official npm installs during update

* fix(plugins): sync official clawhub installs during update

* test(update): mock official plugin sync helpers

---------

Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
This commit is contained in:
Vincent Koc
2026-05-05 17:27:32 -07:00
committed by GitHub
parent 813fe0a3be
commit 2014c2327b
6 changed files with 378 additions and 19 deletions

View File

@@ -253,6 +253,84 @@ describe("collectMissingPluginInstallPayloads", () => {
await fs.rm(tmpDir, { recursive: true, force: true });
}
});
it("keeps disabled trusted official npm records eligible for payload repair when requested", async () => {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-plugin-payload-"));
const missingDir = path.join(tmpDir, "state", "npm", "node_modules", "@openclaw", "codex");
try {
await expect(
collectMissingPluginInstallPayloads({
env: { HOME: tmpDir } as NodeJS.ProcessEnv,
skipDisabledPlugins: true,
syncOfficialPluginInstalls: true,
config: {
plugins: {
entries: {
codex: {
enabled: false,
},
},
},
},
records: {
codex: {
source: "npm",
spec: "@openclaw/codex@2026.5.3",
resolvedName: "@openclaw/codex",
resolvedSpec: "@openclaw/codex@2026.5.3",
installPath: missingDir,
},
},
}),
).resolves.toEqual([
{
pluginId: "codex",
installPath: missingDir,
reason: "missing-package-dir",
},
]);
} finally {
await fs.rm(tmpDir, { recursive: true, force: true });
}
});
it("keeps disabled trusted official ClawHub records eligible for payload repair when requested", async () => {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-plugin-payload-"));
const missingDir = path.join(tmpDir, "state", "clawhub", "diagnostics-otel");
try {
await expect(
collectMissingPluginInstallPayloads({
env: { HOME: tmpDir } as NodeJS.ProcessEnv,
skipDisabledPlugins: true,
syncOfficialPluginInstalls: true,
config: {
plugins: {
entries: {
"diagnostics-otel": {
enabled: false,
},
},
},
},
records: {
"diagnostics-otel": {
source: "clawhub",
spec: "clawhub:@openclaw/diagnostics-otel@2026.5.3",
installPath: missingDir,
},
},
}),
).resolves.toEqual([
{
pluginId: "diagnostics-otel",
installPath: missingDir,
reason: "missing-package-dir",
},
]);
} finally {
await fs.rm(tmpDir, { recursive: true, force: true });
}
});
});
describe("shouldUseLegacyProcessRestartAfterUpdate", () => {

View File

@@ -58,6 +58,8 @@ import {
withPluginInstallRecords,
} from "../../plugins/installed-plugin-index-records.js";
import {
resolveTrustedSourceLinkedOfficialClawHubSpec,
resolveTrustedSourceLinkedOfficialNpmSpec,
syncPluginsForUpdateChannel,
updateNpmInstalledPlugins,
type PluginUpdateIntegrityDriftParams,
@@ -190,6 +192,7 @@ export async function collectMissingPluginInstallPayloads(params: {
records: Record<string, PluginInstallRecord>;
config?: OpenClawConfig;
skipDisabledPlugins?: boolean;
syncOfficialPluginInstalls?: boolean;
env?: NodeJS.ProcessEnv;
}): Promise<MissingPluginInstallPayload[]> {
const env = params.env ?? process.env;
@@ -204,6 +207,12 @@ export async function collectMissingPluginInstallPayloads(params: {
if (!isTrackedPackageInstallRecord(record)) {
continue;
}
const officialNpmSpec = params.syncOfficialPluginInstalls
? resolveTrustedSourceLinkedOfficialNpmSpec({ pluginId, record })
: undefined;
const officialClawHubSpec = params.syncOfficialPluginInstalls
? resolveTrustedSourceLinkedOfficialClawHubSpec({ pluginId, record })
: undefined;
if (normalizedPluginConfig && params.config) {
const enableState = resolveEffectiveEnableState({
id: pluginId,
@@ -211,7 +220,7 @@ export async function collectMissingPluginInstallPayloads(params: {
config: normalizedPluginConfig,
rootConfig: params.config,
});
if (!enableState.enabled) {
if (!enableState.enabled && !officialNpmSpec && !officialClawHubSpec) {
continue;
}
}
@@ -1168,6 +1177,7 @@ async function updatePluginsAfterCoreUpdate(params: {
records,
config: pluginConfig,
skipDisabledPlugins: true,
syncOfficialPluginInstalls: true,
});
if (missing.length === 0) {
return [];
@@ -1188,6 +1198,21 @@ async function updatePluginsAfterCoreUpdate(params: {
defaultRuntime.log(theme.warn(warning.message));
}
}
const repairResult = await updateNpmInstalledPlugins({
config: pluginConfig,
pluginIds: missingIds,
timeoutMs: params.timeoutMs,
updateChannel: params.channel,
skipDisabledPlugins: true,
syncOfficialPluginInstalls: true,
disableOnFailure: true,
logger: pluginLogger,
onIntegrityDrift: onPluginIntegrityDrift,
});
pluginConfig = repairResult.config;
pluginsChanged ||= repairResult.changed;
npmPluginsChanged ||= repairResult.changed;
pluginUpdateOutcomes.push(...repairResult.outcomes);
return missingIds;
};
@@ -1199,6 +1224,8 @@ async function updatePluginsAfterCoreUpdate(params: {
updateChannel: params.channel,
skipIds: new Set([...syncResult.summary.switchedToNpm, ...missingPayloadIds]),
skipDisabledPlugins: true,
syncOfficialPluginInstalls: true,
disableOnFailure: true,
logger: pluginLogger,
onIntegrityDrift: onPluginIntegrityDrift,
});
@@ -1217,6 +1244,7 @@ async function updatePluginsAfterCoreUpdate(params: {
records: pluginConfig.plugins?.installs ?? {},
config: pluginConfig,
skipDisabledPlugins: true,
syncOfficialPluginInstalls: true,
});
pluginUpdateOutcomes.push(
...remainingMissingPayloads