Skip to content

Commit ff1e828

Browse files
fix(update): skip disabled plugins during post-update sync
1 parent 28ff82d commit ff1e828

5 files changed

Lines changed: 172 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
1313

1414
### Fixes
1515

16+
- CLI/update: skip tracked plugins disabled in config during post-update plugin sync before npm, ClawHub, or marketplace update checks, preserving their install records without failing the update. Fixes #73880. Thanks @islandpreneur007.
1617
- Telegram/exec approvals: stop treating general Telegram chat allowlists and `defaultTo` routes as native exec approvers; Telegram now uses explicit `execApprovals.approvers` or owner identity from `commands.ownerAllowFrom`, matching the first-pairing owner bootstrap path. Thanks @pashpashpash.
1718
- Chat commands: route sensitive group `/diagnostics` and `/export-trajectory` approvals and results to a private owner route, preferring same-surface DMs before falling back to the first configured owner route, so Discord group invocations can land in Telegram when that is the primary owner interface. Thanks @pashpashpash.
1819
- Plugin SDK/Discord: restore a deprecated `openclaw/plugin-sdk/discord` compatibility facade and the legacy compat group-policy warning export for the published `@openclaw/discord@2026.3.13` package, covering its config, account, directory, status, and thread-binding imports while keeping new plugins on generic SDK subpaths. Fixes #73685; supersedes #73703. Thanks @rderickson9 and @SymbolStar.

src/cli/update-cli.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1935,10 +1935,14 @@ describe("update-cli", () => {
19351935
const syncConfig = vi.mocked(syncPluginsForUpdateChannel).mock.calls[0]?.[0]?.config as
19361936
| OpenClawConfig
19371937
| undefined;
1938+
const updateCall = vi.mocked(updateNpmInstalledPlugins).mock.calls[0]?.[0] as
1939+
| { skipDisabledPlugins?: boolean }
1940+
| undefined;
19381941
expect(syncConfig?.plugins?.installs).toEqual(pluginInstallRecords);
19391942
expect(syncConfig?.update?.channel).toBe("beta");
19401943
expect(syncConfig?.gateway?.auth).toBeUndefined();
19411944
expect(syncConfig?.plugins?.entries).toBeUndefined();
1945+
expect(updateCall?.skipDisabledPlugins).toBe(true);
19421946
});
19431947

19441948
it("persists channel and runs post-update work after switching from package to git", async () => {

src/cli/update-cli/update-command.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,7 @@ async function updatePluginsAfterCoreUpdate(params: {
742742
config: pluginConfig,
743743
timeoutMs: params.timeoutMs,
744744
skipIds: new Set(syncResult.summary.switchedToNpm),
745+
skipDisabledPlugins: true,
745746
logger: pluginLogger,
746747
onIntegrityDrift: async (drift) => {
747748
integrityDrifts.push({

src/plugins/update.test.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,151 @@ describe("updateNpmInstalledPlugins", () => {
641641
expect(installPluginFromNpmSpecMock).toHaveBeenCalledTimes(1);
642642
});
643643

644+
it.each([
645+
{
646+
source: "npm",
647+
config: {
648+
plugins: {
649+
entries: {
650+
demo: {
651+
enabled: false,
652+
config: { preserved: true },
653+
},
654+
},
655+
installs: {
656+
demo: {
657+
source: "npm" as const,
658+
spec: "@acme/demo",
659+
installPath: "/tmp/demo",
660+
resolvedName: "@acme/demo",
661+
},
662+
},
663+
},
664+
} satisfies OpenClawConfig,
665+
},
666+
{
667+
source: "ClawHub",
668+
config: {
669+
plugins: {
670+
entries: {
671+
demo: {
672+
enabled: false,
673+
config: { preserved: true },
674+
},
675+
},
676+
installs: {
677+
demo: {
678+
source: "clawhub" as const,
679+
spec: "clawhub:demo",
680+
installPath: "/tmp/demo",
681+
clawhubUrl: "https://clawhub.ai",
682+
clawhubPackage: "demo",
683+
clawhubFamily: "code-plugin",
684+
clawhubChannel: "official",
685+
},
686+
},
687+
},
688+
} satisfies OpenClawConfig,
689+
},
690+
{
691+
source: "marketplace",
692+
config: {
693+
plugins: {
694+
entries: {
695+
demo: {
696+
enabled: false,
697+
config: { preserved: true },
698+
},
699+
},
700+
installs: {
701+
demo: {
702+
source: "marketplace" as const,
703+
installPath: "/tmp/demo",
704+
marketplaceSource: "acme/plugins",
705+
marketplacePlugin: "demo",
706+
},
707+
},
708+
},
709+
} satisfies OpenClawConfig,
710+
},
711+
])("skips disabled $source installs before update network calls", async ({ config }) => {
712+
installPluginFromNpmSpecMock.mockRejectedValue(new Error("npm installer should not run"));
713+
installPluginFromClawHubMock.mockRejectedValue(new Error("ClawHub installer should not run"));
714+
installPluginFromMarketplaceMock.mockRejectedValue(
715+
new Error("marketplace installer should not run"),
716+
);
717+
718+
const result = await updateNpmInstalledPlugins({
719+
config,
720+
skipDisabledPlugins: true,
721+
});
722+
723+
expect(runCommandWithTimeoutMock).not.toHaveBeenCalled();
724+
expect(installPluginFromNpmSpecMock).not.toHaveBeenCalled();
725+
expect(installPluginFromClawHubMock).not.toHaveBeenCalled();
726+
expect(installPluginFromMarketplaceMock).not.toHaveBeenCalled();
727+
expect(result.changed).toBe(false);
728+
expect(result.config).toBe(config);
729+
expect(result.config.plugins?.installs?.demo).toEqual(config.plugins.installs.demo);
730+
expect(result.config.plugins?.entries?.demo).toEqual({
731+
enabled: false,
732+
config: { preserved: true },
733+
});
734+
expect(result.outcomes).toEqual([
735+
{
736+
pluginId: "demo",
737+
status: "skipped",
738+
message: 'Skipping "demo" (disabled in config).',
739+
},
740+
]);
741+
});
742+
743+
it("keeps enabled tracked plugin update failures fatal when disabled skipping is enabled", async () => {
744+
installPluginFromNpmSpecMock.mockResolvedValue({
745+
ok: false,
746+
error: "registry timeout",
747+
});
748+
const config = {
749+
plugins: {
750+
entries: {
751+
demo: {
752+
enabled: true,
753+
},
754+
},
755+
installs: {
756+
demo: {
757+
source: "npm" as const,
758+
spec: "@acme/demo",
759+
installPath: "/tmp/demo",
760+
},
761+
},
762+
},
763+
} satisfies OpenClawConfig;
764+
765+
const result = await updateNpmInstalledPlugins({
766+
config,
767+
skipDisabledPlugins: true,
768+
dryRun: true,
769+
});
770+
771+
expect(installPluginFromNpmSpecMock).toHaveBeenCalledWith(
772+
expect.objectContaining({
773+
spec: "@acme/demo",
774+
expectedPluginId: "demo",
775+
dryRun: true,
776+
}),
777+
);
778+
expect(result.changed).toBe(false);
779+
expect(result.config).toBe(config);
780+
expect(result.outcomes).toEqual([
781+
{
782+
pluginId: "demo",
783+
status: "error",
784+
message: "Failed to check demo: registry timeout",
785+
},
786+
]);
787+
});
788+
644789
it("aborts exact pinned npm plugin updates on integrity drift by default", async () => {
645790
const warn = vi.fn();
646791
installPluginFromNpmSpecMock.mockImplementation(

src/plugins/update.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,7 @@ export async function updateNpmInstalledPlugins(params: {
469469
logger?: PluginUpdateLogger;
470470
pluginIds?: string[];
471471
skipIds?: Set<string>;
472+
skipDisabledPlugins?: boolean;
472473
timeoutMs?: number;
473474
dryRun?: boolean;
474475
dangerouslyForceUnsafeInstall?: boolean;
@@ -478,6 +479,9 @@ export async function updateNpmInstalledPlugins(params: {
478479
const logger = params.logger ?? {};
479480
const installs = params.config.plugins?.installs ?? {};
480481
const targets = params.pluginIds?.length ? params.pluginIds : Object.keys(installs);
482+
const normalizedPluginConfig = params.skipDisabledPlugins
483+
? normalizePluginsConfig(params.config.plugins)
484+
: undefined;
481485
const outcomes: PluginUpdateOutcome[] = [];
482486
let next = params.config;
483487
let changed = false;
@@ -502,6 +506,23 @@ export async function updateNpmInstalledPlugins(params: {
502506
continue;
503507
}
504508

509+
if (normalizedPluginConfig) {
510+
const enableState = resolveEffectiveEnableState({
511+
id: pluginId,
512+
origin: "global",
513+
config: normalizedPluginConfig,
514+
rootConfig: params.config,
515+
});
516+
if (!enableState.enabled) {
517+
outcomes.push({
518+
pluginId,
519+
status: "skipped",
520+
message: `Skipping "${pluginId}" (${enableState.reason ?? "disabled by plugin config"}).`,
521+
});
522+
continue;
523+
}
524+
}
525+
505526
if (record.source !== "npm" && record.source !== "marketplace" && record.source !== "clawhub") {
506527
outcomes.push({
507528
pluginId,

0 commit comments

Comments
 (0)