fix(plugins): eagerly install bundled runtime deps#70138
Conversation
🔒 Aisle Security AnalysisWe found 3 potential security issue(s) in this PR:
1. 🟠 Postinstall triggers network `npm install` for bundled plugin deps by default (unpinned ranges, no lockfile)
DescriptionThe packaged install path now eagerly installs bundled plugin runtime dependencies during Impact:
Vulnerable behavior (key excerpts): function shouldEagerInstallBundledPluginDeps(env = process.env) {
const normalized = env?.[EAGER_BUNDLED_PLUGIN_DEPS_ENV]?.trim().toLowerCase();
return !normalized || !DISABLED_EAGER_BUNDLED_PLUGIN_DEPS_VALUES.has(normalized);
}
export function createBundledRuntimeDependencyInstallEnv(env = process.env) {
return {
...createNestedNpmInstallEnv(env),
npm_config_legacy_peer_deps: "true",
npm_config_package_lock: "false",
npm_config_save: "false",
};
}
export function createBundledRuntimeDependencyInstallArgs(missingSpecs) {
return ["install", "--ignore-scripts", ...missingSpecs];
}And the eager install is executed in RecommendationTreat eager installation of dependencies as an opt-in operation and make installs reproducible. Recommended changes:
export function createBundledRuntimeDependencyInstallEnv(env = process.env) {
return {
...createNestedNpmInstallEnv(env),
npm_config_ignore_scripts: "true", // defense-in-depth
npm_config_audit: "false",
npm_config_fund: "false",
npm_config_update_notifier: "false",
// consider pinning/controlling registry if appropriate:
// npm_config_registry: "https://registry.npmjs.org/",
};
}
export function createBundledRuntimeDependencyInstallArgs(missingSpecs) {
return ["ci", "--ignore-scripts", ...missingSpecs];
}Also ensure the bundled plugin 2. 🟠 Path traversal and arbitrary install target via unvalidated dependency name in bundled plugin postinstall
Description
As a result, a malicious bundled plugin manifest can provide a dependency name such as
Vulnerable code: function dependencySentinelPath(depName) {
return join("node_modules", ...depName.split("/"), "package.json");
}
...
.map((dep) => `${dep.name}@${dep.version}`);RecommendationValidate bundled runtime dependency names before using them in paths or building Recommended approach:
Example: import validateName from "validate-npm-package-name";
import { join, resolve, relative, isAbsolute } from "node:path";
function assertSafePackageName(name) {
const result = validateName(name);
if (!result.validForOldPackages && !result.validForNewPackages) {
throw new Error(`unsafe bundled runtime dependency name: ${name}`);
}
}
function safeDependencySentinelPath(packageRoot, depName) {
assertSafePackageName(depName);
const sentinel = join(packageRoot, "node_modules", ...depName.split("/"), "package.json");
const rel = relative(join(packageRoot, "node_modules"), resolve(sentinel));
if (rel.startsWith("..") || isAbsolute(rel)) {
throw new Error(`dependency sentinel path escape for ${depName}`);
}
return sentinel;
}Also consider building install specs using npm's 3. 🟡 Path traversal via unvalidated bundled runtime dependency name in sentinel path construction
Description
An attacker who can influence a bundled plugin manifest could supply a dependency name containing path traversal segments (e.g. Impact:
Vulnerable code: function dependencySentinelPath(depName) {
return join("node_modules", ...depName.split("/"), "package.json");
}Data flow:
RecommendationValidate dependency names before using them in filesystem paths (and ideally before forming install specs). Suggested approach:
Example fix: import validatePackageName from "validate-npm-package-name";
function assertSafeBundledRuntimeDependencyName(name) {
if (typeof name !== "string") throw new Error("invalid dependency name");
if (name.includes("\\")) throw new Error(`unsafe dependency name: ${name}`);
for (const segment of name.split("/")) {
if (segment === "." || segment === ".." || segment.length === 0) {
throw new Error(`unsafe dependency name: ${name}`);
}
}
const result = validatePackageName(name);
if (!result.validForNewPackages) {
throw new Error(`unsafe dependency name: ${name}`);
}
}
// in discoverBundledPluginRuntimeDeps loop:
assertSafeBundledRuntimeDependencyName(name);
assertSafeBundledRuntimeDependencySpec(name, version);This keeps Analyzed PR: #70138 at commit Last updated on: 2026-04-22T12:20:48Z |
Greptile SummaryThis PR changes bundled plugin runtime dependency installation from opt-in (only when Confidence Score: 5/5Safe to merge; the behavior change is well-scoped, covered by targeted tests, and the only remaining note is a P2 suggestion to restore npm stderr in the error message. All findings are P2 or lower. The core logic change (opt-out default, fail-fast, spawn-error surfacing) is correct and directly tested. The previously flagged result.error issue is resolved. The only remaining observation is that npm stderr/stdout is no longer included in failure messages, which affects diagnosability but not correctness. No files require special attention. Prompt To Fix All With AIThis is a comment left during a code review.
Path: scripts/postinstall-bundled-plugins.mjs
Line: 553-554
Comment:
**npm stderr/stdout dropped from failure message**
The old code included `result.stderr` / `result.stdout` in the thrown error (`throw new Error(output || "npm install failed")`), which helped users diagnose why a registry/network install failed. The new path only surfaces the exit code (e.g. `"npm install failed (exit 1)"`), so transient or permanent network errors will appear as opaque failures. The new test confirms the "network unavailable" stderr is intentionally excluded from the expected message.
Consider appending captured output so the postinstall failure is self-diagnosable without needing to re-run:
```suggestion
if (result.status !== 0) {
const output = [result.stderr, result.stdout].filter(Boolean).join("\n").trim();
throw new Error(output ? `npm install failed (exit ${result.status ?? "unknown"}): ${output}` : `npm install failed (exit ${result.status ?? "unknown"})`);
}
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (2): Last reviewed commit: "fix(plugins): harden bundled runtime dep..." | Re-trigger Greptile |
|
@aisle-research-bot @greptile-apps please re-review. Follow-up changes since the last review:
Latest commit on this branch:
|
🔒 Aisle Security AnalysisWe found 1 potential security issue(s) in this PR:
1. 🟠 Bundled plugin dependency spec validation can be bypassed to install from git/alias sources
Description
As a result, npm-supported non-registry specifiers can still pass validation and cause
Because the postinstall script runs during packaged installs and eagerly installs these dependencies, a compromised bundled plugin manifest could trigger fetching and installing attacker-controlled code outside the registry constraints the check appears to intend. Vulnerable code: function isSafeBundledRuntimeDependencySpec(spec) {
const lower = normalized.toLowerCase();
if (lower.startsWith("file:") || /* ... */ lower.startsWith("https:")) {
return false;
}
if (normalized.includes("://") || normalized.startsWith("/") || normalized.startsWith("\\")) {
return false;
}
return true;
}RecommendationUse npm’s own spec parser and implement an allowlist of registry-only spec forms. For example, with import npa from "npm-package-arg";
function assertRegistryOnly(depName, spec) {
const parsed = npa(`${depName}@${spec}`);
// Accept only registry dependencies.
if (parsed.type !== "tag" && parsed.type !== "version" && parsed.type !== "range") {
throw new Error(`unsafe bundled runtime dependency spec for ${depName}: ${spec}`);
}
// Additionally disallow aliases if you want to prevent indirection.
if (parsed.subSpec) {
throw new Error(`unsafe bundled runtime dependency spec for ${depName}: ${spec}`);
}
}This blocks git shorthands ( Analyzed PR: #70138 at commit Last updated on: 2026-04-22T11:35:08Z |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 458fcd97db
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@aisle-research-bot thanks — I reviewed the latest findings against the intended scope of this PR. What I changed in response:
What I am intentionally deferring from this PR:
Why defer:
Verification still green after the local-path hardening:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ac1e6f0374
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Thanks for the quick green fix here. I’m going to close this one because the shape moves us in the wrong direction. The important bit: we already repair bundled runtime dependencies at Gateway/plugin startup before importing enabled bundled plugins:
This PR changes packaged postinstall to eagerly install all missing bundled plugin runtime deps from the OpenClaw package root. So the right fix should stay targeted/plugin-local/external-stage, or adjust the bootstrap/status path so it does not import an unprepared bundled entry. We should not add bundled plugin runtime deps to the root package install path. |
Summary
Cannot find module ...beforeopenclaw doctor --fixcould help.openclaw status, onboarding, or bundled channel entry loading immediately crashed.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
Root Cause (if applicable)
dist/extensions/*/node_modules/**, and normal postinstall runs skipped bundled runtime dependency installation unless an eager-install env var was set.Regression Test Plan (if applicable)
test/scripts/postinstall-bundled-plugins.test.tssrc/plugins/stage-bundled-plugin-runtime-deps.test.tsOPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS=0still worksUser-visible / Behavior Changes
Diagram (if applicable)
Security Impact (required)
No)No)Yes)No)No)Yes, explain risk + mitigation:Repro + Verification
Environment
Steps
pnpm test test/scripts/postinstall-bundled-plugins.test.ts src/plugins/stage-bundled-plugin-runtime-deps.test.tsnpm pack --ignore-scriptsthennpm installthe tarball into a temp prefixnode scripts/test-built-bundled-channel-entry-smoke.mjs --package-root <installed package root>Expected
Actual
Evidence
Human Verification (required)
What you personally verified (not just CI), and how:
pnpm test test/scripts/postinstall-bundled-plugins.test.ts src/plugins/stage-bundled-plugin-runtime-deps.test.tsOPENCLAW_DISABLE_BUNDLED_ENTRY_SOURCE_FALLBACK=1 node scripts/test-built-bundled-channel-entry-smoke.mjs --package-root <installed package root>passesOPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS=0pnpm check:changedis still blocked on unrelated existingtsgo:core:testfailures insrc/agents/pi-embedded-runner/*Review Conversations
Compatibility / Migration
Yes)Yes)No)OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS=0|false|offnow explicitly disables the default eager packaged-install behavior.Risks and Mitigations