mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-07 07:58:36 +00:00
fix(plugins): prevent managed npm peer materialization
This commit is contained in:
@@ -6,6 +6,7 @@ describe("safe npm install helpers", () => {
|
||||
expect(
|
||||
createSafeNpmInstallArgs({
|
||||
omitDev: true,
|
||||
omitPeer: true,
|
||||
ignoreWorkspaces: true,
|
||||
loglevel: "error",
|
||||
noAudit: true,
|
||||
@@ -14,6 +15,7 @@ describe("safe npm install helpers", () => {
|
||||
).toEqual([
|
||||
"install",
|
||||
"--omit=dev",
|
||||
"--omit=peer",
|
||||
"--loglevel=error",
|
||||
"--ignore-scripts",
|
||||
"--workspaces=false",
|
||||
|
||||
@@ -14,6 +14,7 @@ type SafeNpmInstallArgsOptions = {
|
||||
noAudit?: boolean;
|
||||
noFund?: boolean;
|
||||
omitDev?: boolean;
|
||||
omitPeer?: boolean;
|
||||
};
|
||||
|
||||
export function createSafeNpmInstallEnv(
|
||||
@@ -46,6 +47,7 @@ export function createSafeNpmInstallArgs(options: SafeNpmInstallArgsOptions = {}
|
||||
return [
|
||||
"install",
|
||||
...(options.omitDev ? ["--omit=dev"] : []),
|
||||
...(options.omitPeer ? ["--omit=peer"] : []),
|
||||
...(options.loglevel ? [`--loglevel=${options.loglevel}`] : []),
|
||||
"--ignore-scripts",
|
||||
...(options.ignoreWorkspaces ? ["--workspaces=false"] : []),
|
||||
|
||||
@@ -54,6 +54,7 @@ function expectNpmInstallIntoRoot(params: { calls: unknown[][]; npmRoot: string
|
||||
"npm",
|
||||
"install",
|
||||
"--omit=dev",
|
||||
"--omit=peer",
|
||||
"--loglevel=error",
|
||||
"--ignore-scripts",
|
||||
"--no-audit",
|
||||
@@ -145,6 +146,7 @@ type MockNpmPackage = {
|
||||
versions?: string[];
|
||||
installedVersion?: string;
|
||||
installedIntegrity?: string;
|
||||
materializesRootOpenClaw?: boolean;
|
||||
skipLockfileEntry?: boolean;
|
||||
};
|
||||
|
||||
@@ -166,6 +168,11 @@ function writeNpmRootPackageLock(params: {
|
||||
version: pkg.installedVersion ?? pkg.version,
|
||||
integrity: pkg.installedIntegrity ?? pkg.integrity ?? "sha512-plugin-test",
|
||||
};
|
||||
if (pkg.materializesRootOpenClaw) {
|
||||
lockPackages["node_modules/openclaw"] = {
|
||||
version: "2026.5.3",
|
||||
};
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(
|
||||
path.join(params.npmRoot, "package-lock.json"),
|
||||
@@ -190,6 +197,7 @@ function mockNpmViewAndInstall(params: {
|
||||
versions?: string[];
|
||||
installedVersion?: string;
|
||||
installedIntegrity?: string;
|
||||
materializesRootOpenClaw?: boolean;
|
||||
skipLockfileEntry?: boolean;
|
||||
}) {
|
||||
mockNpmViewAndInstallMany([params]);
|
||||
@@ -255,6 +263,15 @@ function mockNpmViewAndInstallMany(packages: MockNpmPackage[]) {
|
||||
...pkg,
|
||||
version: pkg.installedVersion ?? pkg.version,
|
||||
});
|
||||
if (pkg.materializesRootOpenClaw) {
|
||||
const openclawRoot = path.join(npmRoot, "node_modules", "openclaw");
|
||||
fs.mkdirSync(openclawRoot, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(openclawRoot, "package.json"),
|
||||
JSON.stringify({ name: "openclaw", version: "2026.5.3" }),
|
||||
"utf-8",
|
||||
);
|
||||
}
|
||||
installedPackages.push(pkg);
|
||||
}
|
||||
writeNpmRootPackageLock({
|
||||
@@ -553,6 +570,39 @@ describe("installPluginFromNpmSpec", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"repairs root openclaw materialized by npm peer handling and links the host peer",
|
||||
async () => {
|
||||
const stateDir = suiteTempRootTracker.makeTempDir();
|
||||
const npmRoot = path.join(stateDir, "npm");
|
||||
|
||||
mockNpmViewAndInstall({
|
||||
spec: "peer-plugin@1.0.0",
|
||||
packageName: "peer-plugin",
|
||||
version: "1.0.0",
|
||||
pluginId: "peer-plugin",
|
||||
npmRoot,
|
||||
peerDependencies: { openclaw: "^2026.0.0" },
|
||||
materializesRootOpenClaw: true,
|
||||
});
|
||||
|
||||
const result = await installPluginFromNpmSpec({
|
||||
spec: "peer-plugin@1.0.0",
|
||||
npmDir: npmRoot,
|
||||
trustedManagedNpmRoot: true,
|
||||
logger: { info: () => {}, warn: () => {} },
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(fs.existsSync(path.join(npmRoot, "node_modules", "openclaw"))).toBe(false);
|
||||
expect(
|
||||
fs
|
||||
.lstatSync(path.join(npmRoot, "node_modules", "peer-plugin", "node_modules", "openclaw"))
|
||||
.isSymbolicLink(),
|
||||
).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it("repairs stale managed openclaw root packages before npm plugin installs", async () => {
|
||||
const stateDir = suiteTempRootTracker.makeTempDir();
|
||||
const npmRoot = path.join(stateDir, "npm");
|
||||
|
||||
@@ -744,6 +744,14 @@ type ValidatedPackagePlugin = {
|
||||
peerDependencies: Record<string, string>;
|
||||
};
|
||||
|
||||
function resolveOpenClawHostLinkDependencies(manifest: PackageManifest): Record<string, string> {
|
||||
const spec =
|
||||
manifest.peerDependencies?.openclaw ??
|
||||
manifest.dependencies?.openclaw ??
|
||||
manifest.optionalDependencies?.openclaw;
|
||||
return spec ? { openclaw: spec } : {};
|
||||
}
|
||||
|
||||
async function validatePackagePluginInstallSource(params: {
|
||||
runtime: Awaited<ReturnType<typeof loadPluginInstallRuntime>>;
|
||||
packageDir: string;
|
||||
@@ -901,7 +909,7 @@ async function validatePackagePluginInstallSource(params: {
|
||||
version: typeof manifest.version === "string" ? manifest.version : undefined,
|
||||
extensions,
|
||||
hasRuntimeDependencies: hasPackageRuntimeDependencies(manifest),
|
||||
peerDependencies: manifest.peerDependencies ?? {},
|
||||
peerDependencies: resolveOpenClawHostLinkDependencies(manifest),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -913,7 +921,15 @@ async function scanAndLinkInstalledPackage(params: {
|
||||
pluginId: string;
|
||||
peerDependencies: Record<string, string>;
|
||||
logger: PluginInstallLogger;
|
||||
linkOpenClawBeforeScan?: boolean;
|
||||
}): Promise<Extract<InstallPluginResult, { ok: false }> | null> {
|
||||
if (params.linkOpenClawBeforeScan) {
|
||||
await linkOpenClawPeerDependencies({
|
||||
installedDir: params.installedDir,
|
||||
peerDependencies: params.peerDependencies,
|
||||
logger: params.logger,
|
||||
});
|
||||
}
|
||||
const scanResult = await runInstallSourceScan({
|
||||
subject: `Plugin "${params.pluginId}"`,
|
||||
scan: async () =>
|
||||
@@ -929,11 +945,13 @@ async function scanAndLinkInstalledPackage(params: {
|
||||
if (scanResult) {
|
||||
return scanResult;
|
||||
}
|
||||
await linkOpenClawPeerDependencies({
|
||||
installedDir: params.installedDir,
|
||||
peerDependencies: params.peerDependencies,
|
||||
logger: params.logger,
|
||||
});
|
||||
if (!params.linkOpenClawBeforeScan) {
|
||||
await linkOpenClawPeerDependencies({
|
||||
installedDir: params.installedDir,
|
||||
peerDependencies: params.peerDependencies,
|
||||
logger: params.logger,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -966,6 +984,7 @@ export async function installPluginFromInstalledPackageDir(
|
||||
pluginId: validated.plugin.pluginId,
|
||||
peerDependencies: validated.plugin.peerDependencies,
|
||||
logger,
|
||||
linkOpenClawBeforeScan: params.dependencyScanRootDir !== undefined,
|
||||
});
|
||||
if (postInstallError) {
|
||||
return postInstallError;
|
||||
@@ -1216,6 +1235,40 @@ export async function installPluginFromFile(params: {
|
||||
return buildFileInstallResult(pluginId, preparedTarget.targetPath);
|
||||
}
|
||||
|
||||
async function repairManagedNpmRootOpenClawPeerForInstall(params: {
|
||||
logger: PluginInstallLogger;
|
||||
npmRoot: string;
|
||||
phase: "before npm install" | "after npm install";
|
||||
timeoutMs: number;
|
||||
trustedManagedNpmRoot?: boolean;
|
||||
}): Promise<void> {
|
||||
const repairedOpenClawPeer = await repairManagedNpmRootOpenClawPeer({
|
||||
defaultNpmRoot: resolveDefaultPluginNpmDir(),
|
||||
env: createSafeNpmInstallEnv(process.env, { packageLock: true, quiet: true }),
|
||||
hostPackageRoot: resolveOpenClawPackageRootSync({
|
||||
argv1: process.argv[1],
|
||||
moduleUrl: import.meta.url,
|
||||
cwd: process.cwd(),
|
||||
}),
|
||||
npmRoot: params.npmRoot,
|
||||
runCommand: runCommandWithTimeout,
|
||||
timeoutMs: params.timeoutMs,
|
||||
trustedByInstallRecord: params.trustedManagedNpmRoot,
|
||||
});
|
||||
for (const warning of repairedOpenClawPeer.warnings) {
|
||||
params.logger.warn?.(warning);
|
||||
}
|
||||
if (repairedOpenClawPeer.status === "repaired") {
|
||||
params.logger.info?.(
|
||||
`Repaired stale openclaw peer dependency in ${params.npmRoot} ${params.phase}`,
|
||||
);
|
||||
} else if (repairedOpenClawPeer.status === "skipped") {
|
||||
params.logger.warn?.(
|
||||
`Skipped stale openclaw peer repair in ${params.npmRoot} ${params.phase}: ${repairedOpenClawPeer.reason ?? "unproven managed npm root"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function installPluginFromNpmSpec(
|
||||
params: InstallSafetyOverrides & {
|
||||
spec: string;
|
||||
@@ -1340,29 +1393,13 @@ export async function installPluginFromNpmSpec(
|
||||
|
||||
logger.info?.(`Installing ${spec} into ${npmRoot}…`);
|
||||
if (parsedSpec.name !== "openclaw") {
|
||||
const repairedOpenClawPeer = await repairManagedNpmRootOpenClawPeer({
|
||||
defaultNpmRoot: resolveDefaultPluginNpmDir(),
|
||||
env: createSafeNpmInstallEnv(process.env, { packageLock: true, quiet: true }),
|
||||
hostPackageRoot: resolveOpenClawPackageRootSync({
|
||||
argv1: process.argv[1],
|
||||
moduleUrl: import.meta.url,
|
||||
cwd: process.cwd(),
|
||||
}),
|
||||
await repairManagedNpmRootOpenClawPeerForInstall({
|
||||
logger,
|
||||
npmRoot,
|
||||
runCommand: runCommandWithTimeout,
|
||||
phase: "before npm install",
|
||||
timeoutMs,
|
||||
trustedByInstallRecord: params.trustedManagedNpmRoot,
|
||||
trustedManagedNpmRoot: params.trustedManagedNpmRoot,
|
||||
});
|
||||
for (const warning of repairedOpenClawPeer.warnings) {
|
||||
logger.warn?.(warning);
|
||||
}
|
||||
if (repairedOpenClawPeer.status === "repaired") {
|
||||
logger.info?.(`Repaired stale openclaw peer dependency in ${npmRoot}`);
|
||||
} else if (repairedOpenClawPeer.status === "skipped") {
|
||||
logger.warn?.(
|
||||
`Skipped stale openclaw peer repair in ${npmRoot}: ${repairedOpenClawPeer.reason ?? "unproven managed npm root"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
await upsertManagedNpmRootDependency({
|
||||
npmRoot,
|
||||
@@ -1377,6 +1414,7 @@ export async function installPluginFromNpmSpec(
|
||||
"npm",
|
||||
...createSafeNpmInstallArgs({
|
||||
omitDev: true,
|
||||
omitPeer: true,
|
||||
loglevel: "error",
|
||||
noAudit: true,
|
||||
noFund: true,
|
||||
@@ -1401,6 +1439,16 @@ export async function installPluginFromNpmSpec(
|
||||
};
|
||||
}
|
||||
|
||||
if (parsedSpec.name !== "openclaw") {
|
||||
await repairManagedNpmRootOpenClawPeerForInstall({
|
||||
logger,
|
||||
npmRoot,
|
||||
phase: "after npm install",
|
||||
timeoutMs,
|
||||
trustedManagedNpmRoot: params.trustedManagedNpmRoot,
|
||||
});
|
||||
}
|
||||
|
||||
let installedDependency: ManagedNpmRootInstalledDependency | null;
|
||||
try {
|
||||
installedDependency = await readManagedNpmRootInstalledDependency({
|
||||
|
||||
Reference in New Issue
Block a user