diff --git a/src/plugins/install.test.ts b/src/plugins/install.test.ts index be498e3c584..88c765fbf3f 100644 --- a/src/plugins/install.test.ts +++ b/src/plugins/install.test.ts @@ -2387,7 +2387,9 @@ describe("linkOpenClawPeerDependencies (via installPluginFromDir)", () => { const { result } = await installFromDirWithWarnings({ pluginDir, extensionsDir }); expect(result.ok).toBe(true); - if (!result.ok) return; + if (!result.ok) { + return; + } const symlinkPath = path.join(result.targetDir, "node_modules", "openclaw"); const stat = fs.lstatSync(symlinkPath); @@ -2404,7 +2406,9 @@ describe("linkOpenClawPeerDependencies (via installPluginFromDir)", () => { const { result } = await installFromDirWithWarnings({ pluginDir, extensionsDir }); expect(result.ok).toBe(true); - if (!result.ok) return; + if (!result.ok) { + return; + } const nodeModulesDir = path.join(result.targetDir, "node_modules"); const symlinkPath = path.join(nodeModulesDir, "openclaw"); @@ -2431,7 +2435,9 @@ describe("linkOpenClawPeerDependencies (via installPluginFromDir)", () => { expect(second.ok).toBe(true); expect(warnings).toHaveLength(0); - if (!second.ok) return; + if (!second.ok) { + return; + } const symlinkPath = path.join(second.targetDir, "node_modules", "openclaw"); expect(fs.lstatSync(symlinkPath).isSymbolicLink()).toBe(true); }); diff --git a/src/plugins/install.ts b/src/plugins/install.ts index 0ddc527dae3..a886863c4e7 100644 --- a/src/plugins/install.ts +++ b/src/plugins/install.ts @@ -836,15 +836,13 @@ async function installPluginFromPackageDir( } }, afterInstall: async (installedDir) => { - // Symlink any openclaw peerDependencies into the plugin's node_modules/ - // so that plugins declaring openclaw as a peerDependency (rather than a - // regular dependency) can resolve it at runtime. - await linkOpenClawPeerDependencies({ - installedDir, - peerDependencies: peerDeps, - logger, - }); - return await runInstallSourceScan({ + // Run the dependency-tree security scan BEFORE linking peer deps. + // The scan rejects any node_modules/ symlink whose target resolves + // outside the install root — a rule our trusted host-openclaw link + // would fail by design. Running the scan first also keeps the check + // honest against malicious plugins, because any pre-existing symlink + // smuggled in by the source would still be present when we walk. + const scanResult = await runInstallSourceScan({ subject: `Plugin "${pluginId}"`, scan: async () => await runtime.scanInstalledPackageDependencyTree({ @@ -853,6 +851,15 @@ async function installPluginFromPackageDir( pluginId, }), }); + if (scanResult) { + return scanResult; + } + await linkOpenClawPeerDependencies({ + installedDir, + peerDependencies: peerDeps, + logger, + }); + return null; }, }); }