mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:20:43 +00:00
fix: prune legacy plugin runtime deps on install
This commit is contained in:
@@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Fixes
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- Channels: keep Matrix and Mattermost bundled in the core package instead of advertising external npm installs before those channels are cut over. Thanks @vincentkoc.
|
||||
|
||||
@@ -18,8 +18,16 @@ import {
|
||||
unlinkSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { basename, dirname, isAbsolute, join, posix, relative } from "node:path";
|
||||
import { homedir, tmpdir } from "node:os";
|
||||
import {
|
||||
basename,
|
||||
dirname,
|
||||
isAbsolute,
|
||||
join,
|
||||
posix,
|
||||
relative,
|
||||
resolve as pathResolve,
|
||||
} from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
@@ -27,6 +35,7 @@ const DEFAULT_PACKAGE_ROOT = join(__dirname, "..");
|
||||
const DISABLE_POSTINSTALL_ENV = "OPENCLAW_DISABLE_BUNDLED_PLUGIN_POSTINSTALL";
|
||||
const DISABLE_PLUGIN_REGISTRY_MIGRATION_ENV = "OPENCLAW_DISABLE_PLUGIN_REGISTRY_MIGRATION";
|
||||
const DIST_INVENTORY_PATH = "dist/postinstall-inventory.json";
|
||||
const LEGACY_PLUGIN_RUNTIME_DEPS_DIR = "plugin-runtime-deps";
|
||||
const BAILEYS_MEDIA_FILE = join(
|
||||
"node_modules",
|
||||
"@whiskeysockets",
|
||||
@@ -107,6 +116,30 @@ function normalizeRelativePath(filePath) {
|
||||
return filePath.replace(/\\/g, "/");
|
||||
}
|
||||
|
||||
function resolvePostinstallOsHomeDir(env, getHomedir = homedir) {
|
||||
return env?.HOME?.trim() || env?.USERPROFILE?.trim() || getHomedir();
|
||||
}
|
||||
|
||||
function resolvePostinstallTildePath(input, homeDir) {
|
||||
if (input === "~") {
|
||||
return homeDir;
|
||||
}
|
||||
if (input.startsWith("~/") || input.startsWith("~\\")) {
|
||||
return join(homeDir, input.slice(2));
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
function resolvePostinstallOpenClawHomeDir(env, getHomedir = homedir) {
|
||||
const osHome = resolvePostinstallOsHomeDir(env, getHomedir);
|
||||
const override = env?.OPENCLAW_HOME?.trim();
|
||||
return override ? pathResolve(resolvePostinstallTildePath(override, osHome)) : osHome;
|
||||
}
|
||||
|
||||
function resolvePostinstallUserPath(input, openClawHome) {
|
||||
return pathResolve(resolvePostinstallTildePath(input, openClawHome));
|
||||
}
|
||||
|
||||
function readInstalledDistInventory(params = {}) {
|
||||
const packageRoot = params.packageRoot ?? DEFAULT_PACKAGE_ROOT;
|
||||
const pathExists = params.existsSync ?? existsSync;
|
||||
@@ -298,6 +331,75 @@ function pruneLegacyInstalledPluginDependencyDirs(params) {
|
||||
return removed;
|
||||
}
|
||||
|
||||
function splitPostinstallPathList(value) {
|
||||
return value
|
||||
? value
|
||||
.split(pathDelimiter)
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
}
|
||||
|
||||
const pathDelimiter = process.platform === "win32" ? ";" : ":";
|
||||
|
||||
export function collectLegacyPluginRuntimeDepsStateRoots(params = {}) {
|
||||
const env = params.env ?? process.env;
|
||||
const getHomedir = params.homedir ?? homedir;
|
||||
const openClawHome = resolvePostinstallOpenClawHomeDir(env, getHomedir);
|
||||
const stateRoots = [];
|
||||
const addStateRoot = (root) => {
|
||||
if (root) {
|
||||
stateRoots.push(join(root, LEGACY_PLUGIN_RUNTIME_DEPS_DIR));
|
||||
}
|
||||
};
|
||||
|
||||
const stateOverride = env?.OPENCLAW_STATE_DIR?.trim();
|
||||
if (stateOverride) {
|
||||
addStateRoot(resolvePostinstallUserPath(stateOverride, openClawHome));
|
||||
}
|
||||
const configPath = env?.OPENCLAW_CONFIG_PATH?.trim();
|
||||
if (configPath) {
|
||||
addStateRoot(dirname(resolvePostinstallUserPath(configPath, openClawHome)));
|
||||
}
|
||||
addStateRoot(join(openClawHome, ".openclaw"));
|
||||
addStateRoot(join(openClawHome, ".clawdbot"));
|
||||
|
||||
for (const entry of splitPostinstallPathList(env?.STATE_DIRECTORY)) {
|
||||
addStateRoot(resolvePostinstallUserPath(entry, openClawHome));
|
||||
}
|
||||
|
||||
return [...new Set(stateRoots.map((root) => pathResolve(root)))].toSorted((left, right) =>
|
||||
left.localeCompare(right),
|
||||
);
|
||||
}
|
||||
|
||||
export function pruneLegacyPluginRuntimeDepsState(params = {}) {
|
||||
const pathExists = params.existsSync ?? existsSync;
|
||||
const removePath = params.rmSync ?? rmSync;
|
||||
const log = params.log ?? console;
|
||||
const removed = [];
|
||||
|
||||
for (const root of collectLegacyPluginRuntimeDepsStateRoots(params)) {
|
||||
if (!pathExists(root)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
removePath(root, { recursive: true, force: true, maxRetries: 2, retryDelay: 100 });
|
||||
removed.push(root);
|
||||
} catch (error) {
|
||||
log.warn?.(
|
||||
`[postinstall] could not prune legacy plugin runtime deps ${root}: ${String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (removed.length > 0) {
|
||||
log.log?.(`[postinstall] pruned legacy plugin runtime deps: ${removed.join(", ")}`);
|
||||
}
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
const JS_DIST_FILE_RE = /^dist\/.*\.(?:cjs|js|mjs)$/u;
|
||||
|
||||
function stripSpecifierSuffix(value) {
|
||||
@@ -828,6 +930,13 @@ export function runBundledPluginPostinstall(params = {}) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
pruneLegacyPluginRuntimeDepsState({
|
||||
env,
|
||||
existsSync: pathExists,
|
||||
rmSync: params.rmSync,
|
||||
log,
|
||||
homedir: params.homedir,
|
||||
});
|
||||
pruneInstalledPackageDist({
|
||||
packageRoot,
|
||||
existsSync: pathExists,
|
||||
|
||||
@@ -4,10 +4,12 @@ import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
collectLegacyPluginRuntimeDepsStateRoots,
|
||||
isSourceCheckoutRoot,
|
||||
isDirectPostinstallInvocation,
|
||||
pruneOpenClawCompileCache,
|
||||
pruneInstalledPackageDist,
|
||||
pruneLegacyPluginRuntimeDepsState,
|
||||
pruneBundledPluginSourceNodeModules,
|
||||
runBundledPluginPostinstall,
|
||||
runPluginRegistryPostinstallMigration,
|
||||
@@ -208,6 +210,25 @@ describe("bundled plugin postinstall", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("does not prune user-state legacy runtime deps during source-checkout postinstall", async () => {
|
||||
const packageRoot = await createTempDirAsync("openclaw-source-checkout-state-skip-");
|
||||
const home = await createTempDirAsync("openclaw-source-checkout-home-");
|
||||
const legacyRuntimeRoot = path.join(home, ".openclaw", "plugin-runtime-deps");
|
||||
await fs.mkdir(path.join(packageRoot, ".git"), { recursive: true });
|
||||
await fs.mkdir(path.join(packageRoot, "src"), { recursive: true });
|
||||
await fs.mkdir(path.join(packageRoot, "extensions"), { recursive: true });
|
||||
await fs.mkdir(legacyRuntimeRoot, { recursive: true });
|
||||
await fs.writeFile(path.join(legacyRuntimeRoot, "package.json"), "{}\n");
|
||||
|
||||
runBundledPluginPostinstall({
|
||||
env: { HOME: home },
|
||||
packageRoot,
|
||||
log: { log: vi.fn(), warn: vi.fn() },
|
||||
});
|
||||
|
||||
await expect(fs.stat(legacyRuntimeRoot)).resolves.toBeTruthy();
|
||||
});
|
||||
|
||||
it("honors disable env before source-checkout pruning", async () => {
|
||||
const packageRoot = await createTempDirAsync("openclaw-source-checkout-disabled-");
|
||||
const extensionsDir = path.join(packageRoot, "extensions");
|
||||
@@ -373,6 +394,103 @@ describe("bundled plugin postinstall", () => {
|
||||
await expect(fs.stat(staleFile)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
});
|
||||
|
||||
it("prunes legacy plugin runtime deps state during packaged postinstall", async () => {
|
||||
const packageRoot = await createTempDirAsync("openclaw-packaged-state-cleanup-");
|
||||
const home = await createTempDirAsync("openclaw-packaged-home-");
|
||||
const stateOverride = path.join(home, "custom-state");
|
||||
const systemState = path.join(home, "system-state");
|
||||
const defaultLegacyRoot = path.join(home, ".openclaw", "plugin-runtime-deps");
|
||||
const oldBrandLegacyRoot = path.join(home, ".clawdbot", "plugin-runtime-deps");
|
||||
const overrideLegacyRoot = path.join(stateOverride, "plugin-runtime-deps");
|
||||
const systemLegacyRoot = path.join(systemState, "plugin-runtime-deps");
|
||||
const thirdPartyNodeModules = path.join(
|
||||
home,
|
||||
".openclaw",
|
||||
"extensions",
|
||||
"lossless-claw",
|
||||
"node_modules",
|
||||
);
|
||||
const currentFile = path.join(packageRoot, "dist", "entry.js");
|
||||
|
||||
await fs.mkdir(path.dirname(currentFile), { recursive: true });
|
||||
await fs.writeFile(currentFile, "export {};\n");
|
||||
await writePackageDistInventory(packageRoot);
|
||||
for (const root of [
|
||||
defaultLegacyRoot,
|
||||
oldBrandLegacyRoot,
|
||||
overrideLegacyRoot,
|
||||
systemLegacyRoot,
|
||||
thirdPartyNodeModules,
|
||||
]) {
|
||||
await fs.mkdir(root, { recursive: true });
|
||||
await fs.writeFile(path.join(root, "package.json"), "{}\n");
|
||||
}
|
||||
|
||||
const log = { log: vi.fn(), warn: vi.fn() };
|
||||
runBundledPluginPostinstall({
|
||||
env: {
|
||||
HOME: home,
|
||||
OPENCLAW_STATE_DIR: stateOverride,
|
||||
STATE_DIRECTORY: systemState,
|
||||
},
|
||||
packageRoot,
|
||||
log,
|
||||
});
|
||||
|
||||
await expect(fs.stat(defaultLegacyRoot)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
await expect(fs.stat(oldBrandLegacyRoot)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
await expect(fs.stat(overrideLegacyRoot)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
await expect(fs.stat(systemLegacyRoot)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
await expect(fs.stat(thirdPartyNodeModules)).resolves.toBeTruthy();
|
||||
expect(log.warn).not.toHaveBeenCalled();
|
||||
expect(log.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("[postinstall] pruned legacy plugin runtime deps:"),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps legacy plugin runtime deps cleanup non-fatal", () => {
|
||||
const warn = vi.fn();
|
||||
|
||||
expect(() =>
|
||||
pruneLegacyPluginRuntimeDepsState({
|
||||
env: { HOME: "/home/alice" },
|
||||
existsSync: vi.fn(() => true),
|
||||
rmSync: vi.fn(() => {
|
||||
throw new Error("locked");
|
||||
}),
|
||||
log: { log: vi.fn(), warn },
|
||||
homedir: () => "/home/alice",
|
||||
}),
|
||||
).not.toThrow();
|
||||
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
"[postinstall] could not prune legacy plugin runtime deps /home/alice/.openclaw/plugin-runtime-deps: Error: locked",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves legacy plugin runtime deps roots from OpenClaw state env", () => {
|
||||
expect(
|
||||
collectLegacyPluginRuntimeDepsStateRoots({
|
||||
env: {
|
||||
HOME: "/users/alice",
|
||||
OPENCLAW_HOME: "/srv/openclaw-home",
|
||||
OPENCLAW_CONFIG_PATH: "~/profile/openclaw.json",
|
||||
OPENCLAW_STATE_DIR: "~/state",
|
||||
STATE_DIRECTORY: "/var/lib/openclaw",
|
||||
},
|
||||
homedir: () => "/users/alice",
|
||||
}),
|
||||
).toEqual([
|
||||
"/srv/openclaw-home/.clawdbot/plugin-runtime-deps",
|
||||
"/srv/openclaw-home/.openclaw/plugin-runtime-deps",
|
||||
"/srv/openclaw-home/profile/plugin-runtime-deps",
|
||||
"/srv/openclaw-home/state/plugin-runtime-deps",
|
||||
"/var/lib/openclaw/plugin-runtime-deps",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps imported dist chunks even when inventory is stale", async () => {
|
||||
const packageRoot = await createTempDirAsync("openclaw-packaged-install-import-");
|
||||
const entryFile = path.join(packageRoot, "dist", "cli", "run-main.js");
|
||||
|
||||
Reference in New Issue
Block a user