fix(plugin-sdk): scan dependency tree before materialising openclaw symlink

The dependency-tree security scan rejects node_modules symlinks whose
targets resolve outside the install root. Our trusted host-to-plugin
symlink violates that rule by design, so running the scan AFTER
linkOpenClawPeerDependencies would fail every install with
SECURITY_SCAN_FAILED.

Reorder afterInstall so the scan runs first (walking only the plugin's
own staged source, catching any pre-existing malicious openclaw-named
symlink a source might smuggle in), then the trusted link is
materialised on the now-safe tree.

Also use braces on guard clauses in the new unit tests to satisfy the
oxlint no-unreachable-single-statement-if rule.
This commit is contained in:
Anish Kataria
2026-04-23 01:03:00 -04:00
committed by Peter Steinberger
parent 56dd249a07
commit 44820f859e
2 changed files with 25 additions and 12 deletions

View File

@@ -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);
});

View File

@@ -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;
},
});
}