Skip to content

Commit 616a4e9

Browse files
committed
fix(plugins): restore preferred clawhub installs
1 parent fe107d5 commit 616a4e9

3 files changed

Lines changed: 106 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai
3131
- Gateway/performance: keep raw channel-config schema parsing from discovering bundled plugin runtime metadata, and add `pnpm gateway:watch --benchmark-no-force` for profiling startup without the default port cleanup.
3232
- Plugins/onboarding: let Manual setup install optional official plugins, including ClawHub-backed diagnostics with npm fallback, and expose the external Codex plugin as a selectable provider setup choice. Thanks @vincentkoc.
3333
- Plugins/update: treat official externalized bundled npm migrations and ClawHub-to-npm fallbacks as trusted source-linked installs, so prerelease-only official plugin packages can migrate from bundled builds without being rejected as unsafe prerelease resolutions. Thanks @vincentkoc.
34+
- Plugins/update: move ClawHub-preferred externalized plugin installs back to ClawHub after an earlier npm fallback once the ClawHub package becomes available. Thanks @vincentkoc.
3435
- Plugins/update: clean stale bundled load paths for already-externalized pinned npm and ClawHub plugin installs, so release-channel sync does not leave removed bundled paths ahead of the installed external package. Thanks @vincentkoc.
3536
- Plugins/CLI: include package dependency install state in `openclaw plugins list --json` so scripts can spot missing plugin dependencies without runtime-loading plugins.
3637
- Google Meet: preserve `realtime.introMessage: ""` so realtime Chrome joins can stay silent instead of restoring the default spoken intro. Thanks @vincentkoc.

src/plugins/update.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2330,6 +2330,63 @@ describe("syncPluginsForUpdateChannel", () => {
23302330
);
23312331
});
23322332

2333+
it("moves ClawHub-preferred externalized plugin fallbacks back to ClawHub", async () => {
2334+
resolveBundledPluginSourcesMock.mockReturnValue(new Map());
2335+
installPluginFromClawHubMock.mockResolvedValue(
2336+
createSuccessfulClawHubUpdateResult({
2337+
pluginId: "legacy-chat",
2338+
targetDir: "/tmp/openclaw-plugins/legacy-chat",
2339+
version: "2026.5.1-beta.2",
2340+
clawhubPackage: "legacy-chat",
2341+
}),
2342+
);
2343+
2344+
const result = await syncPluginsForUpdateChannel({
2345+
channel: "stable",
2346+
externalizedBundledPluginBridges: [
2347+
{
2348+
bundledPluginId: "legacy-chat",
2349+
preferredSource: "clawhub",
2350+
clawhubSpec: "clawhub:legacy-chat@2026.5.1-beta.2",
2351+
npmSpec: "@openclaw/legacy-chat",
2352+
channelIds: ["legacy-chat"],
2353+
},
2354+
],
2355+
config: {
2356+
channels: {
2357+
"legacy-chat": {
2358+
enabled: true,
2359+
},
2360+
},
2361+
plugins: {
2362+
installs: {
2363+
"legacy-chat": {
2364+
source: "npm",
2365+
spec: "@openclaw/legacy-chat",
2366+
installPath: "/tmp/openclaw-plugins/legacy-chat",
2367+
},
2368+
},
2369+
},
2370+
},
2371+
});
2372+
2373+
expect(installPluginFromClawHubMock).toHaveBeenCalledWith(
2374+
expect.objectContaining({
2375+
spec: "clawhub:legacy-chat@2026.5.1-beta.2",
2376+
mode: "update",
2377+
expectedPluginId: "legacy-chat",
2378+
}),
2379+
);
2380+
expect(installPluginFromNpmSpecMock).not.toHaveBeenCalled();
2381+
expect(result.changed).toBe(true);
2382+
expect(result.summary.switchedToClawHub).toEqual(["legacy-chat"]);
2383+
expect(result.config.plugins?.installs?.["legacy-chat"]).toMatchObject({
2384+
source: "clawhub",
2385+
spec: "clawhub:legacy-chat@2026.5.1-beta.2",
2386+
installPath: "/tmp/openclaw-plugins/legacy-chat",
2387+
});
2388+
});
2389+
23332390
it("fails closed without npm fallback when ClawHub returns integrity drift", async () => {
23342391
resolveBundledPluginSourcesMock.mockReturnValue(new Map());
23352392
installPluginFromClawHubMock.mockResolvedValue({

src/plugins/update.ts

Lines changed: 48 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,36 @@ function isTrustedSourceLinkedOfficialBridgeNpmInstall(params: {
492492
return Boolean(officialPackageName && requestedPackageName === officialPackageName);
493493
}
494494

495+
function isBridgeNpmInstall(params: {
496+
bridge: ExternalizedBundledPluginBridge;
497+
record: PluginInstallRecord;
498+
}): boolean {
499+
const npmSpec = getExternalizedBundledPluginNpmSpec(params.bridge);
500+
if (!npmSpec || params.record.source !== "npm") {
501+
return false;
502+
}
503+
const bridgePackageName = resolveNpmSpecPackageName(npmSpec);
504+
const recordPackageName =
505+
params.record.resolvedName ??
506+
resolveNpmSpecPackageName(params.record.spec) ??
507+
resolveNpmSpecPackageName(params.record.resolvedSpec);
508+
return Boolean(bridgePackageName && recordPackageName === bridgePackageName);
509+
}
510+
511+
function isBridgeClawHubInstall(params: {
512+
bridge: ExternalizedBundledPluginBridge;
513+
record: PluginInstallRecord;
514+
}): boolean {
515+
if (params.record.source !== "clawhub") {
516+
return false;
517+
}
518+
const clawhubSpec = getExternalizedBundledPluginClawHubSpec(params.bridge);
519+
const bridgeClawHubPackage = clawhubSpec ? parseClawHubPluginSpec(clawhubSpec)?.name : undefined;
520+
const recordClawHubPackage =
521+
params.record.clawhubPackage ?? parseClawHubPluginSpec(params.record.spec ?? "")?.name;
522+
return Boolean(bridgeClawHubPackage && recordClawHubPackage === bridgeClawHubPackage);
523+
}
524+
495525
function resolveNpmUpdateSpecs(params: {
496526
record: PluginInstallRecord;
497527
specOverride?: string;
@@ -576,28 +606,20 @@ function isBridgeAlreadyInstalledFromPreferredSource(params: {
576606
bridge: ExternalizedBundledPluginBridge;
577607
record: PluginInstallRecord;
578608
}): boolean {
579-
const npmSpec = getExternalizedBundledPluginNpmSpec(params.bridge);
580-
if (npmSpec && params.record.source === "npm") {
581-
const bridgePackageName = resolveNpmSpecPackageName(npmSpec);
582-
const recordPackageName =
583-
params.record.resolvedName ??
584-
resolveNpmSpecPackageName(params.record.spec) ??
585-
resolveNpmSpecPackageName(params.record.resolvedSpec);
586-
if (bridgePackageName && recordPackageName === bridgePackageName) {
587-
return true;
588-
}
589-
}
590-
const clawhubSpec = getExternalizedBundledPluginClawHubSpec(params.bridge);
591-
const bridgeClawHubPackage = clawhubSpec ? parseClawHubPluginSpec(clawhubSpec)?.name : undefined;
592-
const recordClawHubPackage =
593-
params.record.source === "clawhub"
594-
? (params.record.clawhubPackage ?? parseClawHubPluginSpec(params.record.spec ?? "")?.name)
595-
: undefined;
596-
return Boolean(
597-
bridgeClawHubPackage &&
598-
params.record.source === "clawhub" &&
599-
recordClawHubPackage === bridgeClawHubPackage,
600-
);
609+
const preferredSource = getExternalizedBundledPluginPreferredSource(params.bridge);
610+
return preferredSource === "clawhub"
611+
? isBridgeClawHubInstall(params)
612+
: isBridgeNpmInstall(params);
613+
}
614+
615+
function isBridgeInstalledFromFallbackSource(params: {
616+
bridge: ExternalizedBundledPluginBridge;
617+
record: PluginInstallRecord;
618+
}): boolean {
619+
const preferredSource = getExternalizedBundledPluginPreferredSource(params.bridge);
620+
return preferredSource === "clawhub"
621+
? isBridgeNpmInstall(params)
622+
: isBridgeClawHubInstall(params);
601623
}
602624

603625
function replacePluginIdInList(
@@ -1448,6 +1470,10 @@ export async function syncPluginsForUpdateChannel(params: {
14481470
bridge,
14491471
record: existing.record,
14501472
env,
1473+
}) &&
1474+
!isBridgeInstalledFromFallbackSource({
1475+
bridge,
1476+
record: existing.record,
14511477
})
14521478
) {
14531479
continue;

0 commit comments

Comments
 (0)