mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 17:00:50 +00:00
fix(update): skip disabled plugins during post-update sync (#73970)
Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
This commit is contained in:
@@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/reload: bound default restart deferral and SIGUSR1 restart drain to five minutes while preserving explicit `deferralTimeoutMs: 0` indefinite waits, so stale active work accounting cannot block config reloads forever. Thanks @vincentkoc.
|
||||
- Active Memory: register the prompt-build hook with the configured recall timeout plus setup grace instead of the 150s maximum budget, so default memory recall cannot delay turn startup for multiple minutes. Thanks @vincentkoc.
|
||||
- CLI/channels logs: reuse the rolling log-file resolver so `openclaw channels logs` falls back to the active dated log across date boundaries without reading unrelated custom log files. Fixes #42875; carries forward #42904 and #43043. Thanks @ethanclaw and @wdskuki.
|
||||
- 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.
|
||||
- Security/audit: recognize dangerous node command IDs as valid `gateway.nodes.denyCommands` entries, so audit only warns on real typos or unsupported patterns. (#56923) Thanks @chziyue.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
@@ -1935,10 +1935,14 @@ describe("update-cli", () => {
|
||||
const syncConfig = vi.mocked(syncPluginsForUpdateChannel).mock.calls[0]?.[0]?.config as
|
||||
| OpenClawConfig
|
||||
| undefined;
|
||||
const updateCall = vi.mocked(updateNpmInstalledPlugins).mock.calls[0]?.[0] as
|
||||
| { skipDisabledPlugins?: boolean }
|
||||
| undefined;
|
||||
expect(syncConfig?.plugins?.installs).toEqual(pluginInstallRecords);
|
||||
expect(syncConfig?.update?.channel).toBe("beta");
|
||||
expect(syncConfig?.gateway?.auth).toBeUndefined();
|
||||
expect(syncConfig?.plugins?.entries).toBeUndefined();
|
||||
expect(updateCall?.skipDisabledPlugins).toBe(true);
|
||||
});
|
||||
|
||||
it("persists channel and runs post-update work after switching from package to git", async () => {
|
||||
|
||||
@@ -742,6 +742,7 @@ async function updatePluginsAfterCoreUpdate(params: {
|
||||
config: pluginConfig,
|
||||
timeoutMs: params.timeoutMs,
|
||||
skipIds: new Set(syncResult.summary.switchedToNpm),
|
||||
skipDisabledPlugins: true,
|
||||
logger: pluginLogger,
|
||||
onIntegrityDrift: async (drift) => {
|
||||
integrityDrifts.push({
|
||||
|
||||
@@ -641,6 +641,151 @@ describe("updateNpmInstalledPlugins", () => {
|
||||
expect(installPluginFromNpmSpecMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
source: "npm",
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
demo: {
|
||||
enabled: false,
|
||||
config: { preserved: true },
|
||||
},
|
||||
},
|
||||
installs: {
|
||||
demo: {
|
||||
source: "npm" as const,
|
||||
spec: "@acme/demo",
|
||||
installPath: "/tmp/demo",
|
||||
resolvedName: "@acme/demo",
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig,
|
||||
},
|
||||
{
|
||||
source: "ClawHub",
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
demo: {
|
||||
enabled: false,
|
||||
config: { preserved: true },
|
||||
},
|
||||
},
|
||||
installs: {
|
||||
demo: {
|
||||
source: "clawhub" as const,
|
||||
spec: "clawhub:demo",
|
||||
installPath: "/tmp/demo",
|
||||
clawhubUrl: "https://clawhub.ai",
|
||||
clawhubPackage: "demo",
|
||||
clawhubFamily: "code-plugin",
|
||||
clawhubChannel: "official",
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig,
|
||||
},
|
||||
{
|
||||
source: "marketplace",
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
demo: {
|
||||
enabled: false,
|
||||
config: { preserved: true },
|
||||
},
|
||||
},
|
||||
installs: {
|
||||
demo: {
|
||||
source: "marketplace" as const,
|
||||
installPath: "/tmp/demo",
|
||||
marketplaceSource: "acme/plugins",
|
||||
marketplacePlugin: "demo",
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig,
|
||||
},
|
||||
])("skips disabled $source installs before update network calls", async ({ config }) => {
|
||||
installPluginFromNpmSpecMock.mockRejectedValue(new Error("npm installer should not run"));
|
||||
installPluginFromClawHubMock.mockRejectedValue(new Error("ClawHub installer should not run"));
|
||||
installPluginFromMarketplaceMock.mockRejectedValue(
|
||||
new Error("marketplace installer should not run"),
|
||||
);
|
||||
|
||||
const result = await updateNpmInstalledPlugins({
|
||||
config,
|
||||
skipDisabledPlugins: true,
|
||||
});
|
||||
|
||||
expect(runCommandWithTimeoutMock).not.toHaveBeenCalled();
|
||||
expect(installPluginFromNpmSpecMock).not.toHaveBeenCalled();
|
||||
expect(installPluginFromClawHubMock).not.toHaveBeenCalled();
|
||||
expect(installPluginFromMarketplaceMock).not.toHaveBeenCalled();
|
||||
expect(result.changed).toBe(false);
|
||||
expect(result.config).toBe(config);
|
||||
expect(result.config.plugins?.installs?.demo).toEqual(config.plugins.installs.demo);
|
||||
expect(result.config.plugins?.entries?.demo).toEqual({
|
||||
enabled: false,
|
||||
config: { preserved: true },
|
||||
});
|
||||
expect(result.outcomes).toEqual([
|
||||
{
|
||||
pluginId: "demo",
|
||||
status: "skipped",
|
||||
message: 'Skipping "demo" (disabled in config).',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps enabled tracked plugin update failures fatal when disabled skipping is enabled", async () => {
|
||||
installPluginFromNpmSpecMock.mockResolvedValue({
|
||||
ok: false,
|
||||
error: "registry timeout",
|
||||
});
|
||||
const config = {
|
||||
plugins: {
|
||||
entries: {
|
||||
demo: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
installs: {
|
||||
demo: {
|
||||
source: "npm" as const,
|
||||
spec: "@acme/demo",
|
||||
installPath: "/tmp/demo",
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
|
||||
const result = await updateNpmInstalledPlugins({
|
||||
config,
|
||||
skipDisabledPlugins: true,
|
||||
dryRun: true,
|
||||
});
|
||||
|
||||
expect(installPluginFromNpmSpecMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
spec: "@acme/demo",
|
||||
expectedPluginId: "demo",
|
||||
dryRun: true,
|
||||
}),
|
||||
);
|
||||
expect(result.changed).toBe(false);
|
||||
expect(result.config).toBe(config);
|
||||
expect(result.outcomes).toEqual([
|
||||
{
|
||||
pluginId: "demo",
|
||||
status: "error",
|
||||
message: "Failed to check demo: registry timeout",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("aborts exact pinned npm plugin updates on integrity drift by default", async () => {
|
||||
const warn = vi.fn();
|
||||
installPluginFromNpmSpecMock.mockImplementation(
|
||||
|
||||
@@ -469,6 +469,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
logger?: PluginUpdateLogger;
|
||||
pluginIds?: string[];
|
||||
skipIds?: Set<string>;
|
||||
skipDisabledPlugins?: boolean;
|
||||
timeoutMs?: number;
|
||||
dryRun?: boolean;
|
||||
dangerouslyForceUnsafeInstall?: boolean;
|
||||
@@ -478,6 +479,9 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
const logger = params.logger ?? {};
|
||||
const installs = params.config.plugins?.installs ?? {};
|
||||
const targets = params.pluginIds?.length ? params.pluginIds : Object.keys(installs);
|
||||
const normalizedPluginConfig = params.skipDisabledPlugins
|
||||
? normalizePluginsConfig(params.config.plugins)
|
||||
: undefined;
|
||||
const outcomes: PluginUpdateOutcome[] = [];
|
||||
let next = params.config;
|
||||
let changed = false;
|
||||
@@ -502,6 +506,23 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (normalizedPluginConfig) {
|
||||
const enableState = resolveEffectiveEnableState({
|
||||
id: pluginId,
|
||||
origin: "global",
|
||||
config: normalizedPluginConfig,
|
||||
rootConfig: params.config,
|
||||
});
|
||||
if (!enableState.enabled) {
|
||||
outcomes.push({
|
||||
pluginId,
|
||||
status: "skipped",
|
||||
message: `Skipping "${pluginId}" (${enableState.reason ?? "disabled by plugin config"}).`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (record.source !== "npm" && record.source !== "marketplace" && record.source !== "clawhub") {
|
||||
outcomes.push({
|
||||
pluginId,
|
||||
|
||||
Reference in New Issue
Block a user