Skip to content

Commit f754907

Browse files
committed
fix: prune legacy plugin runtime deps on install
1 parent 05c9492 commit f754907

3 files changed

Lines changed: 230 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai
1414
### Fixes
1515

1616
- Control UI/Gateway: avoid full session-list reloads for locally applied message-phase session updates, carry known session keys through transcript-file update events, and defer media provider listing when explicit generation model config is present. Refs #76236, #76203, #76188, #76107, and #76166. Thanks @BunsDev.
17+
- Install/update: prune the obsolete `plugin-runtime-deps` state directory during packaged postinstall so upgrades from pre-2026.5.2 releases reclaim old bundled-plugin dependency caches without touching external plugin installs.
1718
- Gateway: keep directly requested plugin tools invokable under restrictive tool profiles while preserving explicit deny lists and the HTTP safety deny list, preventing catalog/invoke mismatches that surface as "Tool not available". Thanks @BunsDev.
1819
- Gateway/update: allow beta binaries to refresh gateway services when the config was last written by the matching stable release version, avoiding false newer-config downgrade blocks during beta channel updates.
1920
- Channels: keep Matrix and Mattermost bundled in the core package instead of advertising external npm installs before those channels are cut over. Thanks @vincentkoc.

scripts/postinstall-bundled-plugins.mjs

Lines changed: 111 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,24 @@ import {
1818
unlinkSync,
1919
writeFileSync,
2020
} from "node:fs";
21-
import { tmpdir } from "node:os";
22-
import { basename, dirname, isAbsolute, join, posix, relative } from "node:path";
21+
import { homedir, tmpdir } from "node:os";
22+
import {
23+
basename,
24+
dirname,
25+
isAbsolute,
26+
join,
27+
posix,
28+
relative,
29+
resolve as pathResolve,
30+
} from "node:path";
2331
import { fileURLToPath, pathToFileURL } from "node:url";
2432

2533
const __dirname = dirname(fileURLToPath(import.meta.url));
2634
const DEFAULT_PACKAGE_ROOT = join(__dirname, "..");
2735
const DISABLE_POSTINSTALL_ENV = "OPENCLAW_DISABLE_BUNDLED_PLUGIN_POSTINSTALL";
2836
const DISABLE_PLUGIN_REGISTRY_MIGRATION_ENV = "OPENCLAW_DISABLE_PLUGIN_REGISTRY_MIGRATION";
2937
const DIST_INVENTORY_PATH = "dist/postinstall-inventory.json";
38+
const LEGACY_PLUGIN_RUNTIME_DEPS_DIR = "plugin-runtime-deps";
3039
const BAILEYS_MEDIA_FILE = join(
3140
"node_modules",
3241
"@whiskeysockets",
@@ -107,6 +116,30 @@ function normalizeRelativePath(filePath) {
107116
return filePath.replace(/\\/g, "/");
108117
}
109118

119+
function resolvePostinstallOsHomeDir(env, getHomedir = homedir) {
120+
return env?.HOME?.trim() || env?.USERPROFILE?.trim() || getHomedir();
121+
}
122+
123+
function resolvePostinstallTildePath(input, homeDir) {
124+
if (input === "~") {
125+
return homeDir;
126+
}
127+
if (input.startsWith("~/") || input.startsWith("~\\")) {
128+
return join(homeDir, input.slice(2));
129+
}
130+
return input;
131+
}
132+
133+
function resolvePostinstallOpenClawHomeDir(env, getHomedir = homedir) {
134+
const osHome = resolvePostinstallOsHomeDir(env, getHomedir);
135+
const override = env?.OPENCLAW_HOME?.trim();
136+
return override ? pathResolve(resolvePostinstallTildePath(override, osHome)) : osHome;
137+
}
138+
139+
function resolvePostinstallUserPath(input, openClawHome) {
140+
return pathResolve(resolvePostinstallTildePath(input, openClawHome));
141+
}
142+
110143
function readInstalledDistInventory(params = {}) {
111144
const packageRoot = params.packageRoot ?? DEFAULT_PACKAGE_ROOT;
112145
const pathExists = params.existsSync ?? existsSync;
@@ -298,6 +331,75 @@ function pruneLegacyInstalledPluginDependencyDirs(params) {
298331
return removed;
299332
}
300333

334+
function splitPostinstallPathList(value) {
335+
return value
336+
? value
337+
.split(pathDelimiter)
338+
.map((entry) => entry.trim())
339+
.filter(Boolean)
340+
: [];
341+
}
342+
343+
const pathDelimiter = process.platform === "win32" ? ";" : ":";
344+
345+
export function collectLegacyPluginRuntimeDepsStateRoots(params = {}) {
346+
const env = params.env ?? process.env;
347+
const getHomedir = params.homedir ?? homedir;
348+
const openClawHome = resolvePostinstallOpenClawHomeDir(env, getHomedir);
349+
const stateRoots = [];
350+
const addStateRoot = (root) => {
351+
if (root) {
352+
stateRoots.push(join(root, LEGACY_PLUGIN_RUNTIME_DEPS_DIR));
353+
}
354+
};
355+
356+
const stateOverride = env?.OPENCLAW_STATE_DIR?.trim();
357+
if (stateOverride) {
358+
addStateRoot(resolvePostinstallUserPath(stateOverride, openClawHome));
359+
}
360+
const configPath = env?.OPENCLAW_CONFIG_PATH?.trim();
361+
if (configPath) {
362+
addStateRoot(dirname(resolvePostinstallUserPath(configPath, openClawHome)));
363+
}
364+
addStateRoot(join(openClawHome, ".openclaw"));
365+
addStateRoot(join(openClawHome, ".clawdbot"));
366+
367+
for (const entry of splitPostinstallPathList(env?.STATE_DIRECTORY)) {
368+
addStateRoot(resolvePostinstallUserPath(entry, openClawHome));
369+
}
370+
371+
return [...new Set(stateRoots.map((root) => pathResolve(root)))].toSorted((left, right) =>
372+
left.localeCompare(right),
373+
);
374+
}
375+
376+
export function pruneLegacyPluginRuntimeDepsState(params = {}) {
377+
const pathExists = params.existsSync ?? existsSync;
378+
const removePath = params.rmSync ?? rmSync;
379+
const log = params.log ?? console;
380+
const removed = [];
381+
382+
for (const root of collectLegacyPluginRuntimeDepsStateRoots(params)) {
383+
if (!pathExists(root)) {
384+
continue;
385+
}
386+
try {
387+
removePath(root, { recursive: true, force: true, maxRetries: 2, retryDelay: 100 });
388+
removed.push(root);
389+
} catch (error) {
390+
log.warn?.(
391+
`[postinstall] could not prune legacy plugin runtime deps ${root}: ${String(error)}`,
392+
);
393+
}
394+
}
395+
396+
if (removed.length > 0) {
397+
log.log?.(`[postinstall] pruned legacy plugin runtime deps: ${removed.join(", ")}`);
398+
}
399+
400+
return removed;
401+
}
402+
301403
const JS_DIST_FILE_RE = /^dist\/.*\.(?:cjs|js|mjs)$/u;
302404

303405
function stripSpecifierSuffix(value) {
@@ -828,6 +930,13 @@ export function runBundledPluginPostinstall(params = {}) {
828930
});
829931
return;
830932
}
933+
pruneLegacyPluginRuntimeDepsState({
934+
env,
935+
existsSync: pathExists,
936+
rmSync: params.rmSync,
937+
log,
938+
homedir: params.homedir,
939+
});
831940
pruneInstalledPackageDist({
832941
packageRoot,
833942
existsSync: pathExists,

test/scripts/postinstall-bundled-plugins.test.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import { tmpdir } from "node:os";
44
import path from "node:path";
55
import { describe, expect, it, vi } from "vitest";
66
import {
7+
collectLegacyPluginRuntimeDepsStateRoots,
78
isSourceCheckoutRoot,
89
isDirectPostinstallInvocation,
910
pruneOpenClawCompileCache,
1011
pruneInstalledPackageDist,
12+
pruneLegacyPluginRuntimeDepsState,
1113
pruneBundledPluginSourceNodeModules,
1214
runBundledPluginPostinstall,
1315
runPluginRegistryPostinstallMigration,
@@ -208,6 +210,25 @@ describe("bundled plugin postinstall", () => {
208210
);
209211
});
210212

213+
it("does not prune user-state legacy runtime deps during source-checkout postinstall", async () => {
214+
const packageRoot = await createTempDirAsync("openclaw-source-checkout-state-skip-");
215+
const home = await createTempDirAsync("openclaw-source-checkout-home-");
216+
const legacyRuntimeRoot = path.join(home, ".openclaw", "plugin-runtime-deps");
217+
await fs.mkdir(path.join(packageRoot, ".git"), { recursive: true });
218+
await fs.mkdir(path.join(packageRoot, "src"), { recursive: true });
219+
await fs.mkdir(path.join(packageRoot, "extensions"), { recursive: true });
220+
await fs.mkdir(legacyRuntimeRoot, { recursive: true });
221+
await fs.writeFile(path.join(legacyRuntimeRoot, "package.json"), "{}\n");
222+
223+
runBundledPluginPostinstall({
224+
env: { HOME: home },
225+
packageRoot,
226+
log: { log: vi.fn(), warn: vi.fn() },
227+
});
228+
229+
await expect(fs.stat(legacyRuntimeRoot)).resolves.toBeTruthy();
230+
});
231+
211232
it("honors disable env before source-checkout pruning", async () => {
212233
const packageRoot = await createTempDirAsync("openclaw-source-checkout-disabled-");
213234
const extensionsDir = path.join(packageRoot, "extensions");
@@ -373,6 +394,103 @@ describe("bundled plugin postinstall", () => {
373394
await expect(fs.stat(staleFile)).rejects.toMatchObject({ code: "ENOENT" });
374395
});
375396

397+
it("prunes legacy plugin runtime deps state during packaged postinstall", async () => {
398+
const packageRoot = await createTempDirAsync("openclaw-packaged-state-cleanup-");
399+
const home = await createTempDirAsync("openclaw-packaged-home-");
400+
const stateOverride = path.join(home, "custom-state");
401+
const systemState = path.join(home, "system-state");
402+
const defaultLegacyRoot = path.join(home, ".openclaw", "plugin-runtime-deps");
403+
const oldBrandLegacyRoot = path.join(home, ".clawdbot", "plugin-runtime-deps");
404+
const overrideLegacyRoot = path.join(stateOverride, "plugin-runtime-deps");
405+
const systemLegacyRoot = path.join(systemState, "plugin-runtime-deps");
406+
const thirdPartyNodeModules = path.join(
407+
home,
408+
".openclaw",
409+
"extensions",
410+
"lossless-claw",
411+
"node_modules",
412+
);
413+
const currentFile = path.join(packageRoot, "dist", "entry.js");
414+
415+
await fs.mkdir(path.dirname(currentFile), { recursive: true });
416+
await fs.writeFile(currentFile, "export {};\n");
417+
await writePackageDistInventory(packageRoot);
418+
for (const root of [
419+
defaultLegacyRoot,
420+
oldBrandLegacyRoot,
421+
overrideLegacyRoot,
422+
systemLegacyRoot,
423+
thirdPartyNodeModules,
424+
]) {
425+
await fs.mkdir(root, { recursive: true });
426+
await fs.writeFile(path.join(root, "package.json"), "{}\n");
427+
}
428+
429+
const log = { log: vi.fn(), warn: vi.fn() };
430+
runBundledPluginPostinstall({
431+
env: {
432+
HOME: home,
433+
OPENCLAW_STATE_DIR: stateOverride,
434+
STATE_DIRECTORY: systemState,
435+
},
436+
packageRoot,
437+
log,
438+
});
439+
440+
await expect(fs.stat(defaultLegacyRoot)).rejects.toMatchObject({ code: "ENOENT" });
441+
await expect(fs.stat(oldBrandLegacyRoot)).rejects.toMatchObject({ code: "ENOENT" });
442+
await expect(fs.stat(overrideLegacyRoot)).rejects.toMatchObject({ code: "ENOENT" });
443+
await expect(fs.stat(systemLegacyRoot)).rejects.toMatchObject({ code: "ENOENT" });
444+
await expect(fs.stat(thirdPartyNodeModules)).resolves.toBeTruthy();
445+
expect(log.warn).not.toHaveBeenCalled();
446+
expect(log.log).toHaveBeenCalledWith(
447+
expect.stringContaining("[postinstall] pruned legacy plugin runtime deps:"),
448+
);
449+
});
450+
451+
it("keeps legacy plugin runtime deps cleanup non-fatal", () => {
452+
const warn = vi.fn();
453+
454+
expect(() =>
455+
pruneLegacyPluginRuntimeDepsState({
456+
env: { HOME: "/home/alice" },
457+
existsSync: vi.fn(() => true),
458+
rmSync: vi.fn(() => {
459+
throw new Error("locked");
460+
}),
461+
log: { log: vi.fn(), warn },
462+
homedir: () => "/home/alice",
463+
}),
464+
).not.toThrow();
465+
466+
expect(warn).toHaveBeenCalledWith(
467+
expect.stringContaining(
468+
"[postinstall] could not prune legacy plugin runtime deps /home/alice/.openclaw/plugin-runtime-deps: Error: locked",
469+
),
470+
);
471+
});
472+
473+
it("resolves legacy plugin runtime deps roots from OpenClaw state env", () => {
474+
expect(
475+
collectLegacyPluginRuntimeDepsStateRoots({
476+
env: {
477+
HOME: "/users/alice",
478+
OPENCLAW_HOME: "/srv/openclaw-home",
479+
OPENCLAW_CONFIG_PATH: "~/profile/openclaw.json",
480+
OPENCLAW_STATE_DIR: "~/state",
481+
STATE_DIRECTORY: "/var/lib/openclaw",
482+
},
483+
homedir: () => "/users/alice",
484+
}),
485+
).toEqual([
486+
"/srv/openclaw-home/.clawdbot/plugin-runtime-deps",
487+
"/srv/openclaw-home/.openclaw/plugin-runtime-deps",
488+
"/srv/openclaw-home/profile/plugin-runtime-deps",
489+
"/srv/openclaw-home/state/plugin-runtime-deps",
490+
"/var/lib/openclaw/plugin-runtime-deps",
491+
]);
492+
});
493+
376494
it("keeps imported dist chunks even when inventory is stale", async () => {
377495
const packageRoot = await createTempDirAsync("openclaw-packaged-install-import-");
378496
const entryFile = path.join(packageRoot, "dist", "cli", "run-main.js");

0 commit comments

Comments
 (0)