mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
fix(plugins): preserve package deps for runtime mirrors
This commit is contained in:
@@ -718,7 +718,17 @@ function Invoke-OpenClawUpdateWithTimeout {
|
||||
|
||||
$updateJob = Start-Job -ScriptBlock {
|
||||
param([string]$Path, [string]$Target)
|
||||
$output = & $Path update --tag $Target --yes --json *>&1
|
||||
$previousDisableBundledPlugins = $env:OPENCLAW_DISABLE_BUNDLED_PLUGINS
|
||||
$env:OPENCLAW_DISABLE_BUNDLED_PLUGINS = '1'
|
||||
try {
|
||||
$output = & $Path update --tag $Target --yes --json *>&1
|
||||
} finally {
|
||||
if ($null -eq $previousDisableBundledPlugins) {
|
||||
Remove-Item Env:OPENCLAW_DISABLE_BUNDLED_PLUGINS -ErrorAction SilentlyContinue
|
||||
} else {
|
||||
$env:OPENCLAW_DISABLE_BUNDLED_PLUGINS = $previousDisableBundledPlugins
|
||||
}
|
||||
}
|
||||
[pscustomobject]@{
|
||||
ExitCode = $LASTEXITCODE
|
||||
Output = ($output | Out-String).Trim()
|
||||
@@ -1649,7 +1659,7 @@ stop_openclaw_gateway_processes() {
|
||||
# host can observe new plugin metadata mid-update and abort config validation.
|
||||
scrub_future_plugin_entries
|
||||
stop_openclaw_gateway_processes
|
||||
/opt/homebrew/bin/openclaw update --tag "$update_target" --yes --json
|
||||
OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 /opt/homebrew/bin/openclaw update --tag "$update_target" --yes --json
|
||||
# Same-guest npm upgrades can leave the old gateway process holding the old
|
||||
# bundled plugin host version. Stop it before post-update config commands.
|
||||
stop_openclaw_gateway_processes
|
||||
@@ -1782,7 +1792,7 @@ stop_openclaw_gateway_processes() {
|
||||
# the old host can observe new plugin metadata mid-update and abort validation.
|
||||
scrub_future_plugin_entries
|
||||
stop_openclaw_gateway_processes
|
||||
openclaw update --tag "$update_target" --yes --json
|
||||
OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 openclaw update --tag "$update_target" --yes --json
|
||||
# The fresh Linux lane starts a manual gateway; stop the old process before
|
||||
# post-update config validation sees mixed old-host/new-plugin metadata.
|
||||
stop_openclaw_gateway_processes
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import { Module } from "node:module";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
@@ -56,6 +57,8 @@ const BUNDLED_RUNTIME_DEPS_LOCK_TIMEOUT_MS = 5 * 60_000;
|
||||
const BUNDLED_RUNTIME_DEPS_LOCK_STALE_MS = 10 * 60_000;
|
||||
const BUNDLED_RUNTIME_DEPS_OWNERLESS_LOCK_STALE_MS = 30_000;
|
||||
|
||||
const registeredBundledRuntimeDepNodePaths = new Set<string>();
|
||||
|
||||
export type BundledRuntimeDepsNpmRunner = {
|
||||
command: string;
|
||||
args: string[];
|
||||
@@ -440,6 +443,43 @@ function resolveBundledPluginPackageRoot(pluginRoot: string): string | null {
|
||||
return path.dirname(buildDir);
|
||||
}
|
||||
|
||||
export function resolveBundledRuntimeDependencyPackageRoot(pluginRoot: string): string | null {
|
||||
return resolveBundledPluginPackageRoot(pluginRoot);
|
||||
}
|
||||
|
||||
export function registerBundledRuntimeDependencyNodePath(rootDir: string): void {
|
||||
const nodeModulesDir = path.join(rootDir, "node_modules");
|
||||
if (registeredBundledRuntimeDepNodePaths.has(nodeModulesDir) || !fs.existsSync(nodeModulesDir)) {
|
||||
return;
|
||||
}
|
||||
const currentPaths = (process.env.NODE_PATH ?? "")
|
||||
.split(path.delimiter)
|
||||
.map((entry) => entry.trim())
|
||||
.filter((entry) => entry.length > 0);
|
||||
process.env.NODE_PATH = [
|
||||
nodeModulesDir,
|
||||
...currentPaths.filter((entry) => entry !== nodeModulesDir),
|
||||
].join(path.delimiter);
|
||||
(Module as unknown as { _initPaths?: () => void })._initPaths?.();
|
||||
registeredBundledRuntimeDepNodePaths.add(nodeModulesDir);
|
||||
}
|
||||
|
||||
export function clearBundledRuntimeDependencyNodePaths(): void {
|
||||
if (registeredBundledRuntimeDepNodePaths.size === 0) {
|
||||
return;
|
||||
}
|
||||
const retainedPaths = (process.env.NODE_PATH ?? "")
|
||||
.split(path.delimiter)
|
||||
.filter((entry) => entry.length > 0 && !registeredBundledRuntimeDepNodePaths.has(entry));
|
||||
if (retainedPaths.length > 0) {
|
||||
process.env.NODE_PATH = retainedPaths.join(path.delimiter);
|
||||
} else {
|
||||
delete process.env.NODE_PATH;
|
||||
}
|
||||
registeredBundledRuntimeDepNodePaths.clear();
|
||||
(Module as unknown as { _initPaths?: () => void })._initPaths?.();
|
||||
}
|
||||
|
||||
function isPackagedBundledPluginRoot(pluginRoot: string): boolean {
|
||||
const packageRoot = resolveBundledPluginPackageRoot(pluginRoot);
|
||||
return Boolean(packageRoot && !isSourceCheckoutRoot(packageRoot));
|
||||
|
||||
@@ -3,6 +3,8 @@ import path from "node:path";
|
||||
import {
|
||||
ensureBundledPluginRuntimeDeps,
|
||||
resolveBundledRuntimeDependencyInstallRoot,
|
||||
resolveBundledRuntimeDependencyPackageRoot,
|
||||
registerBundledRuntimeDependencyNodePath,
|
||||
} from "./bundled-runtime-deps.js";
|
||||
|
||||
const bundledRuntimeDepsRetainSpecsByInstallRoot = new Map<string, readonly string[]>();
|
||||
@@ -44,6 +46,11 @@ export function prepareBundledPluginRuntimeRoot(params: {
|
||||
if (path.resolve(installRoot) === path.resolve(params.pluginRoot)) {
|
||||
return { pluginRoot: params.pluginRoot, modulePath: params.modulePath };
|
||||
}
|
||||
const packageRoot = resolveBundledRuntimeDependencyPackageRoot(params.pluginRoot);
|
||||
if (packageRoot) {
|
||||
registerBundledRuntimeDependencyNodePath(packageRoot);
|
||||
}
|
||||
registerBundledRuntimeDependencyNodePath(installRoot);
|
||||
const mirrorRoot = mirrorBundledPluginRuntimeRoot({
|
||||
pluginId: params.pluginId,
|
||||
pluginRoot: params.pluginRoot,
|
||||
|
||||
@@ -1558,6 +1558,106 @@ module.exports = {
|
||||
expect(registry.plugins.find((entry) => entry.id === "alpha")?.status).toBe("loaded");
|
||||
});
|
||||
|
||||
it("loads copied external runtime mirrors with package-root runtime deps", () => {
|
||||
const packageRoot = makeTempDir();
|
||||
const stageDir = makeTempDir();
|
||||
const bundledDir = path.join(packageRoot, "dist", "extensions");
|
||||
const pluginRoot = path.join(bundledDir, "alpha");
|
||||
const packageDepRoot = path.join(packageRoot, "node_modules", "root-support");
|
||||
fs.mkdirSync(pluginRoot, { recursive: true });
|
||||
fs.mkdirSync(packageDepRoot, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(packageRoot, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "openclaw",
|
||||
version: "2026.4.24",
|
||||
type: "module",
|
||||
dependencies: { "root-support": "1.0.0" },
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(packageDepRoot, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "root-support",
|
||||
version: "1.0.0",
|
||||
type: "module",
|
||||
exports: "./index.js",
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(packageDepRoot, "index.js"),
|
||||
"export default { marker: 'root-ok' };\n",
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(packageRoot, "dist", "manifest-support.js"),
|
||||
[`import support from "root-support";`, `export const marker = support.marker;`, ""].join(
|
||||
"\n",
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginRoot, "index.js"),
|
||||
[
|
||||
`import { marker } from "../../manifest-support.js";`,
|
||||
`export default {`,
|
||||
` id: "alpha",`,
|
||||
` register(api) {`,
|
||||
` api.registerCommand({ name: "root-support", handler: () => marker });`,
|
||||
` },`,
|
||||
`};`,
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginRoot, "package.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "@openclaw/alpha",
|
||||
version: "1.0.0",
|
||||
type: "module",
|
||||
openclaw: { extensions: ["./index.js"] },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginRoot, "openclaw.plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
id: "alpha",
|
||||
enabledByDefault: true,
|
||||
configSchema: EMPTY_PLUGIN_SCHEMA,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledDir;
|
||||
process.env.OPENCLAW_PLUGIN_STAGE_DIR = stageDir;
|
||||
|
||||
const symlinkSync = vi.spyOn(fs, "symlinkSync").mockImplementation(() => {
|
||||
throw Object.assign(new Error("symlinks unavailable"), { code: "EPERM" });
|
||||
});
|
||||
let registry: PluginRegistry | null = null;
|
||||
try {
|
||||
registry = loadOpenClawPlugins({
|
||||
cache: false,
|
||||
config: { plugins: { enabled: true } },
|
||||
});
|
||||
} finally {
|
||||
symlinkSync.mockRestore();
|
||||
}
|
||||
|
||||
expect(registry?.plugins.find((entry) => entry.id === "alpha")?.status).toBe("loaded");
|
||||
});
|
||||
|
||||
it("loads bundled plugins with plugin-sdk imports from an external stage dir", () => {
|
||||
const packageRoot = makeTempDir();
|
||||
const stageDir = makeTempDir();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import { Module } from "node:module";
|
||||
import path from "node:path";
|
||||
import {
|
||||
clearAgentHarnesses,
|
||||
@@ -33,9 +32,12 @@ import { resolvePluginActivationSourceConfig } from "./activation-source-config.
|
||||
import { buildPluginApi } from "./api-builder.js";
|
||||
import { inspectBundleMcpRuntimeSupport } from "./bundle-mcp.js";
|
||||
import {
|
||||
clearBundledRuntimeDependencyNodePaths,
|
||||
ensureBundledPluginRuntimeDeps,
|
||||
installBundledRuntimeDeps,
|
||||
resolveBundledRuntimeDependencyInstallRoot,
|
||||
resolveBundledRuntimeDependencyPackageRoot,
|
||||
registerBundledRuntimeDependencyNodePath,
|
||||
type BundledRuntimeDepsInstallParams,
|
||||
} from "./bundled-runtime-deps.js";
|
||||
import {
|
||||
@@ -255,6 +257,7 @@ export function clearPluginLoaderCache(): void {
|
||||
inFlightPluginRegistryLoads.clear();
|
||||
openAllowlistWarningCache.clear();
|
||||
clearBundledRuntimeDependencyNodePaths();
|
||||
registeredBundledRuntimeDepMirrorRoots.clear();
|
||||
clearAgentHarnesses();
|
||||
clearPluginCommands();
|
||||
clearCompactionProviders();
|
||||
@@ -484,12 +487,11 @@ function resolveCanonicalDistRuntimeSource(source: string): string {
|
||||
return fs.existsSync(candidate) ? candidate : source;
|
||||
}
|
||||
|
||||
const registeredBundledRuntimeDepNodePaths = new Set<string>();
|
||||
const registeredBundledRuntimeDepMirrorRoots = new Set<string>();
|
||||
|
||||
function isBundledRuntimeDependencyMirrorPath(modulePath: string): boolean {
|
||||
const resolvedModulePath = path.resolve(modulePath);
|
||||
for (const nodeModulesDir of registeredBundledRuntimeDepNodePaths) {
|
||||
const installRoot = path.dirname(nodeModulesDir);
|
||||
for (const installRoot of registeredBundledRuntimeDepMirrorRoots) {
|
||||
if (
|
||||
resolvedModulePath === installRoot ||
|
||||
resolvedModulePath.startsWith(`${installRoot}${path.sep}`)
|
||||
@@ -500,37 +502,8 @@ function isBundledRuntimeDependencyMirrorPath(modulePath: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
function registerBundledRuntimeDependencyNodePath(installRoot: string): void {
|
||||
const nodeModulesDir = path.join(installRoot, "node_modules");
|
||||
if (registeredBundledRuntimeDepNodePaths.has(nodeModulesDir) || !fs.existsSync(nodeModulesDir)) {
|
||||
return;
|
||||
}
|
||||
const currentPaths = (process.env.NODE_PATH ?? "")
|
||||
.split(path.delimiter)
|
||||
.map((entry) => entry.trim())
|
||||
.filter((entry) => entry.length > 0);
|
||||
process.env.NODE_PATH = [
|
||||
nodeModulesDir,
|
||||
...currentPaths.filter((entry) => entry !== nodeModulesDir),
|
||||
].join(path.delimiter);
|
||||
(Module as unknown as { _initPaths?: () => void })._initPaths?.();
|
||||
registeredBundledRuntimeDepNodePaths.add(nodeModulesDir);
|
||||
}
|
||||
|
||||
function clearBundledRuntimeDependencyNodePaths(): void {
|
||||
if (registeredBundledRuntimeDepNodePaths.size === 0) {
|
||||
return;
|
||||
}
|
||||
const retainedPaths = (process.env.NODE_PATH ?? "")
|
||||
.split(path.delimiter)
|
||||
.filter((entry) => entry.length > 0 && !registeredBundledRuntimeDepNodePaths.has(entry));
|
||||
if (retainedPaths.length > 0) {
|
||||
process.env.NODE_PATH = retainedPaths.join(path.delimiter);
|
||||
} else {
|
||||
delete process.env.NODE_PATH;
|
||||
}
|
||||
registeredBundledRuntimeDepNodePaths.clear();
|
||||
(Module as unknown as { _initPaths?: () => void })._initPaths?.();
|
||||
function registerBundledRuntimeDependencyMirrorRoot(installRoot: string): void {
|
||||
registeredBundledRuntimeDepMirrorRoots.add(path.resolve(installRoot));
|
||||
}
|
||||
|
||||
function mirrorBundledPluginRuntimeRoot(params: {
|
||||
@@ -2382,7 +2355,12 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
}
|
||||
}
|
||||
if (path.resolve(installRoot) !== path.resolve(pluginRoot)) {
|
||||
const packageRoot = resolveBundledRuntimeDependencyPackageRoot(pluginRoot);
|
||||
if (packageRoot) {
|
||||
registerBundledRuntimeDependencyNodePath(packageRoot);
|
||||
}
|
||||
registerBundledRuntimeDependencyNodePath(installRoot);
|
||||
registerBundledRuntimeDependencyMirrorRoot(installRoot);
|
||||
runtimePluginRoot = mirrorBundledPluginRuntimeRoot({
|
||||
pluginId: record.id,
|
||||
pluginRoot,
|
||||
|
||||
@@ -21,11 +21,11 @@ describe("parallels npm update smoke", () => {
|
||||
expect(script).toContain("delete entries.whatsapp");
|
||||
expect(script).toContain("Remove-FuturePluginEntries\n Stop-OpenClawGatewayProcesses");
|
||||
expect(script).toContain("scrub_future_plugin_entries\nstop_openclaw_gateway_processes");
|
||||
expect(script).not.toContain("$env:OPENCLAW_DISABLE_BUNDLED_PLUGINS = '1'\n & $Path update");
|
||||
expect(script).not.toContain(
|
||||
expect(script).toContain("$env:OPENCLAW_DISABLE_BUNDLED_PLUGINS = '1'");
|
||||
expect(script).toContain(
|
||||
"OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 /opt/homebrew/bin/openclaw update",
|
||||
);
|
||||
expect(script).not.toContain("OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 openclaw update");
|
||||
expect(script).toContain("OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 openclaw update");
|
||||
expect(script).toContain(
|
||||
"OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 /opt/homebrew/bin/openclaw gateway stop",
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user