fix(plugins): anchor runtime dependency installs

This commit is contained in:
Peter Steinberger
2026-04-25 22:11:53 +01:00
parent 0f58a6597d
commit 20223e02d9
4 changed files with 50 additions and 4 deletions

View File

@@ -117,6 +117,9 @@ Docs: https://docs.openclaw.ai
- Plugins/startup: remove ownerless bundled runtime-dependency install locks
after a short grace window and include lock owner details when startup times
out waiting for a plugin runtime-deps lock.
- Plugins/install: anchor bundled runtime-dependency npm installs with an
OpenClaw-owned package manifest so Linux updates cannot accidentally write to
a parent `$HOME/node_modules` tree. Fixes #71730.
- Live tests/voice: accept common STT variants for OpenClaw and ElevenLabs
brand names so provider smoke tests fail on real regressions rather than
equivalent transcripts.

View File

@@ -222,6 +222,44 @@ describe("installBundledRuntimeDeps", () => {
);
});
it("anchors non-isolated external install roots with a package manifest", () => {
const parentRoot = makeTempDir();
const installRoot = path.join(parentRoot, ".openclaw", "plugin-runtime-deps", "openclaw-test");
fs.mkdirSync(path.join(parentRoot, "node_modules", "@grammyjs"), { recursive: true });
spawnSyncMock.mockImplementation((_command, _args, options) => {
const cwd = String(options?.cwd ?? "");
expect(cwd).toBe(installRoot);
expect(JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8"))).toEqual({
name: "openclaw-runtime-deps-install",
private: true,
});
return {
pid: 123,
output: [],
stdout: "",
stderr: "",
signal: null,
status: 0,
};
});
installBundledRuntimeDeps({
installRoot,
missingSpecs: ["@grammyjs/runner@^2.0.3"],
env: {
HOME: parentRoot,
},
});
expect(spawnSyncMock).toHaveBeenCalledWith(
expect.any(String),
expect.any(Array),
expect.objectContaining({
cwd: installRoot,
}),
);
});
it("uses an isolated execution root and copies node_modules back when requested", () => {
const installRoot = makeTempDir();
const installExecutionRoot = makeTempDir();

View File

@@ -1154,14 +1154,17 @@ function shouldCleanBundledRuntimeDepsInstallExecutionRoot(params: {
return installExecutionRoot.startsWith(`${installRoot}${path.sep}`);
}
function writeBundledRuntimeDepsInstallManifest(installExecutionRoot: string): void {
function ensureNpmInstallExecutionManifest(installExecutionRoot: string): void {
const manifestPath = path.join(installExecutionRoot, "package.json");
if (fs.existsSync(manifestPath)) {
return;
}
fs.writeFileSync(
path.join(installExecutionRoot, "package.json"),
manifestPath,
`${JSON.stringify({ name: "openclaw-runtime-deps-install", private: true }, null, 2)}\n`,
"utf8",
);
}
export function installBundledRuntimeDeps(params: {
installRoot: string;
installExecutionRoot?: string;
@@ -1184,7 +1187,7 @@ export function installBundledRuntimeDeps(params: {
// doctor repair path installs directly in the external stage dir; without a
// manifest, npm can honor a user's global prefix config and write under
// $HOME/node_modules instead of our managed stage.
writeBundledRuntimeDepsInstallManifest(installExecutionRoot);
ensureNpmInstallExecutionManifest(installExecutionRoot);
const installEnv = createBundledRuntimeDepsInstallEnv(params.env, {
cacheDir: path.join(installExecutionRoot, ".openclaw-npm-cache"),
});

View File

@@ -176,6 +176,7 @@ describe("resolveInstalledPluginProviderContributionIds", () => {
resolveInstalledPluginProviderContributionIds({
candidates: [candidate],
env: hermeticEnv(),
preferPersisted: false,
}),
).toEqual(["demo", "demo-alias"]);
});
@@ -197,6 +198,7 @@ describe("resolveInstalledPluginProviderContributionIds", () => {
},
},
env: hermeticEnv(),
preferPersisted: false,
};
expect(resolveInstalledPluginProviderContributionIds(params)).toEqual([]);