Skip to content

fix: restore bundled plugin dep install by default after npm pack#70650

Closed
EronFan wants to merge 12 commits into
openclaw:mainfrom
EronFan:fix/plugin-deps-npm-pack
Closed

fix: restore bundled plugin dep install by default after npm pack#70650
EronFan wants to merge 12 commits into
openclaw:mainfrom
EronFan:fix/plugin-deps-npm-pack

Conversation

@EronFan

@EronFan EronFan commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Bug

After upgrading via npm pack + global install, plugin runtime dependencies (e.g. @larksuiteoapi/node-sdk for Feishu, @whiskeysockets/baileys for WhatsApp) are missing. Onboarding fails.

Root Cause

8a4332864b added shouldEagerInstallBundledPluginDeps() guard returning false by default. f6360da116 moved plugin deps to plugin-local. Result: npm pack tarball excludes plugin node_modules and postinstall skips installing them.

Fix

Remove the shouldEagerInstallBundledPluginDeps() gate. Packaged installs now always run the bundled plugin dep install step. Updated regression test (24 tests pass).

Subagent added 12 commits April 21, 2026 05:24
Root cause: cleanupBundleMcpOnRunEnd was gated behind a flag, causing
MCP stdio servers spawned per session to accumulate unboundedly when
the flag was false or unset. Every session created orphaned MCP processes
consuming RAM (reported 2.4GB from a single context7-mcp instance).

Fix: always call disposeSessionMcpRuntime() in the run finally block,
removing the conditional cleanupBundleMcpOnRunEnd flag and making
session cleanup unconditional. Also removes the now-unnecessary flag
from RunEmbeddedPiAgentParams and cleans up all test references.

Affected files:
- run.ts: remove flag guard, always call disposeSessionMcpRuntime
- params.ts: remove cleanupBundleMcpOnRunEnd from RunEmbeddedPiAgentParams
- attempt-execution.ts / types.ts / agent-via-gateway.ts: remove flag
- e2e.test.ts / agent.test.ts: remove test references to the flag

Fixes openclaw#69465
…nt config

Commit caab5a6 removed the flag from RunEmbeddedPiAgentParams but
missed cleaning up agent.test.ts, agent-via-gateway.test.ts, and
live.test.ts, causing TS2353 at build time.

Also add passWithNoTests to vitest.agents.config.ts so it exits cleanly
when only .live.test.ts files are staged (they are included by the glob
but then excluded by the shared **/*.live.test.ts filter, leaving no files).
Fix openclaw#70234 - Codex ACP subagents fail with ACP_TURN_FAILED because
spawnAcpDirect() accepted model/thinking params but never forwarded
them into the gateway agent request, causing wrong default runtime.

Also add regression tests for both tool layer and gateway call layer.
Targeted tests: 2 files, 58 tests passed.
…ent restart storm

Fix openclaw#70232 - Bonjour mDNS crash causes restart storm on VPS/cloud.
When @homebridge/ciao surfaces CIAO_PROBING_CANCELLED on mDNS-broken
networks, the advertiser/watchdog would keep retrying indefinitely.
Now disables the advertiser, clears the watchdog, and shuts down the
current ciao cycle when a ciao cancellation is seen.

Add regression tests for both unhandled rejection and watchdog re-advertise
paths with CIAO PROBING CANCELLED.
Regressed by commit 8a43328 which added shouldEagerInstallBundledPluginDeps()
guard — bundled plugin deps (feishu/node-sdk, whatsapp/baileys, matrix libs) are
now plugin-local (f6360da) but the eager-install gate disabled them by default
for npm pack installs.

The postinstall now always installs missing plugin runtime deps for packaged
installs (non-source-checkout), matching the original behavior before the
regression. Source checkouts still prune bundled plugin node_modules via the
existing isSourceCheckout() branch.
@EronFan
EronFan requested a review from a team as a code owner April 23, 2026 15:24
@openclaw-barnacle openclaw-barnacle Bot added channel: slack Channel integration: slack scripts Repository scripts commands Command implementations agents Agent runtime and tooling size: L labels Apr 23, 2026
@greptile-apps

greptile-apps Bot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR bundles several independent bug fixes: (1) the headline fix restores bundled plugin dep installation for packaged (npm pack) installs by removing the shouldEagerInstallBundledPluginDeps() gate that was preventing @larksuiteoapi/node-sdk, @whiskeysockets/baileys, etc. from being installed; (2) Telegram DM session keys are migrated from agent:<id>:main to agent:<id>:dm to fix sandbox isolation (#70342); (3) MCP child-process cleanup is made unconditional so gateway-path nested runs no longer leak; (4) Slack provider is fixed to pass undefined instead of "" so the ?? fallback correctly picks up resolved secret tokens; (5) Bonjour advertiser gracefully disables when ciao probing is cancelled on mDNS-unavailable networks.

  • The DM session-key rename (agent:X:mainagent:X:dm) is an intentional, well-documented trade-off that resets existing Telegram DM session history on upgrade in exchange for fixing sandbox isolation; worth confirming this is acceptable for existing users.

Confidence Score: 5/5

Safe to merge; all remaining findings are P2 or lower and the fixes are well-tested with explicit regression tests.

The core postinstall fix is correct — applyBundledPluginRuntimeHotfixes is still called (either in the missingSpecs.length === 0 early-return path or unconditionally after the npm install block), so no hotfix logic is lost. The bonjour null-safety is handled (stopCycle accepts BonjourCycle | null and guards with if (!cycle) return). The cleanupBundleMcpOnRunEnd removal is straightforward. The only notable side-effect is the DM session key migration, which is intentional and documented.

No files require special attention — all 31 changed files look correct.

Reviews (1): Last reviewed commit: "fix: restore bundled plugin dep install ..." | Re-trigger Greptile

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c6f1b6f405

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

}

export default createAgentsVitestConfig();
export default createAgentsVitestConfig({ passWithNoTests: true });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pass passWithNoTests via options, not env override

createAgentsVitestConfig interprets its argument as an env map, so createAgentsVitestConfig({ passWithNoTests: true }) replaces process.env with a one-key object instead of setting Vitest options. This means createScopedVitestConfig can no longer read env-scoped includes (e.g. OPENCLAW_VITEST_INCLUDE_FILE in sharded runs), so the agents project can execute the wrong test scope, and passWithNoTests still isn't applied because it never reaches the options object.

Useful? React with 👍 / 👎.

@aisle-research-bot

aisle-research-bot Bot commented Apr 23, 2026

Copy link
Copy Markdown

🔒 Aisle Security Analysis

We found 3 potential security issue(s) in this PR:

# Severity Title
1 🟠 High Install-time code execution risk: postinstall runs npm install for plugin deps with lifecycle scripts enabled
2 🟡 Medium Missing authorization for user-controlled thinking override in gateway agent handler
3 🔵 Low Unvalidated OpenRouter canonical_slug aliasing allows cache poisoning and unbounded cache growth
1. 🟠 Install-time code execution risk: postinstall runs `npm install` for plugin deps with lifecycle scripts enabled
Property Value
Severity High
CWE CWE-494
Location scripts/postinstall-bundled-plugins.mjs:720-773

Description

runBundledPluginPostinstall() now always installs bundled plugin runtime dependencies for packaged installs by invoking npm install at install time.

Security impact:

  • Dependency specs (missingSpecs) are derived from each bundled plugin's dist/extensions/<pluginId>/package.json (dependencies + optionalDependencies).
  • The script then executes npm install ... <name>@<version> without --ignore-scripts / npm_config_ignore_scripts=true.
  • Any dependency (or its transitive dependencies) can run arbitrary code via npm lifecycle scripts (preinstall, install, postinstall, prepare).
  • Because this runs during postinstall, it expands the attack surface for supply-chain attacks and for scenarios where the packaged plugin manifests can be tampered with before installation (e.g., compromised distribution channel / mirrored artifacts).

Vulnerable code (install-time execution path):

const result = spawn(npmRunner.command, npmRunner.args, {
  cwd: packageRoot,
  encoding: "utf8",
  env: npmRunner.env ?? nestedEnv,
  stdio: "pipe",
  shell: npmRunner.shell,
  windowsVerbatimArguments: npmRunner.windowsVerbatimArguments,
});

and dependency specs are sourced from plugin manifests:

const packageJson = readJsonFile(packageJsonPath);
for (const [name, version] of Object.entries(collectRuntimeDeps(packageJson))) {
  deps.set(name, { name, version, ... });
}

Recommendation

Avoid executing third-party lifecycle scripts during install-time dependency remediation.

Preferred: disable scripts and only install from controlled, integrity-checked sources.

Example hardening:

const nestedEnv = createNestedNpmInstallEnv(env);// Disable lifecycle scripts
nestedEnv.npm_config_ignore_scripts = "true";

const npmRunner = resolveNpmRunner({
  env: nestedEnv,
  ...,
  npmArgs: [
    "install",
    "--omit=dev",
    "--no-save",
    "--package-lock=false",
    "--legacy-peer-deps",
    "--ignore-scripts",
    ...missingSpecs,
  ],
});

Additional mitigations (defense in depth):

  • Use an allowlist of packages/versions permitted to be installed (do not accept arbitrary deps from plugin package.json).
  • Prefer shipping required runtime deps pre-bundled, or use a lockfile with integrity (package-lock.json/npm ci) if network install is unavoidable.
  • Consider pinning/validating registry configuration (npm_config_registry) to a trusted registry and disabling git+* / file: dependency spec types when constructing missingSpecs.
2. 🟡 Missing authorization for user-controlled `thinking` override in gateway agent handler
Property Value
Severity Medium
CWE CWE-285
Location src/gateway/server-methods/agent.ts:357-373

Description

The gateway RPC handler for method: "agent" restricts provider/model overrides to owner/internal callers, but forwards the caller-supplied thinking parameter without any authorization check.

  • request.thinking is accepted from the RPC request.
  • Authorization is only enforced for request.provider/request.model via allowModelOverride.
  • request.thinking is then forwarded into dispatchAgentRunFromGateway() as ingressOpts.thinking.

Impact:

  • Any client that can call the gateway agent method can potentially force higher-cost or less-safe reasoning/thinking modes (depending on how thinking maps to internal behavior), even when model overrides are disallowed.
  • This is a policy/billing control bypass and can facilitate cost escalation or bypass of configured runtime constraints.

Vulnerable code:

const allowModelOverride = resolveAllowModelOverrideFromClient(client);
const requestedModelOverride = Boolean(request.provider || request.model);
if (requestedModelOverride && !allowModelOverride) { /* reject */ }// ... later
thinking: request.thinking,

Recommendation

Enforce authorization and validation for thinking overrides similarly to model/provider overrides.

Suggested approach:

  • Add a separate capability check (e.g., allowThinkingOverride) or reuse allowModelOverride.
  • Reject unauthorized requests that include thinking.
  • Validate thinking against an allowlist (e.g., via the same normalizeThinkLevel() used elsewhere) before dispatch.

Example:

const allowThinkingOverride = resolveSenderIsOwnerFromClient(client) || client?.internal?.allowThinkingOverride === true;
if (request.thinking && !allowThinkingOverride) {
  respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST,
    "thinking override is not authorized for this caller."));
  return;
}
const thinking = request.thinking ? normalizeThinkLevel(request.thinking) : undefined;
if (request.thinking && !thinking) {
  respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST,
    "invalid thinking level"));
  return;
}// ...
thinking,
3. 🔵 Unvalidated OpenRouter canonical_slug aliasing allows cache poisoning and unbounded cache growth
Property Value
Severity Low
CWE CWE-400
Location src/agents/pi-embedded-runner/openrouter-model-capabilities.ts:206-214

Description

doFetch() stores OpenRouter API-provided canonical_slug as an additional key in the capabilities map without validation or collision checks.

This introduces two issues when the OpenRouter catalog response is malicious/compromised (or provided by an attacker-controlled proxy via resolveProxyFetchFromEnv()):

  • Cache poisoning via key collision: if canonical_slug equals another model's id, the later map.set() overwrites the prior entry, causing lookups for the victim ID to return the wrong capabilities.
  • Memory/disk pressure: arbitrary/very long canonical_slug strings (or many unique slugs) increase Map size and the on-disk JSON cache written by writeDiskCache(), with no bounds/length limits.

Vulnerable code:

const parsed = parseModel(model);
map.set(model.id, parsed);
if (model.canonical_slug && model.canonical_slug !== model.id) {
  map.set(model.canonical_slug, parsed);
}

Recommendation

Treat canonical_slug as untrusted input and harden aliasing:

  • Validate/normalize canonical_slug (type, length limit, allowed charset like /^[a-z0-9_.-]+\/[a-z0-9_.-]+$/i depending on expected format).
  • Prevent collisions: only add the alias if it does not already exist in the map, or if it already points to the same parsed record.
  • Bound cache growth: cap total models/aliases processed and reject excessively large responses.

Example:

function isSafeAlias(alias: unknown): alias is string {
  return (
    typeof alias === "string" &&
    alias.length > 0 &&
    alias.length <= 128 &&/^[a-z0-9_.-]+\/[a-z0-9_.-]+$/i.test(alias)
  );
}// ...
map.set(model.id, parsed);
if (isSafeAlias(model.canonical_slug) && model.canonical_slug !== model.id) {
  if (!map.has(model.canonical_slug)) {
    map.set(model.canonical_slug, parsed);
  }
}

Analyzed PR: #70650 at commit c6f1b6f

Last updated on: 2026-04-23T15:35:06Z

@steipete

Copy link
Copy Markdown
Contributor

Closing this for now. The bundled-deps regression it targets is fixed in 2026.4.22, and this PR is too broad/risky to land as a follow-up.

Main concerns:

  • It is conflicted/stale against main.
  • It touches 31 files and mixes unrelated fixes with the bundled-deps change, which makes maintainer review and rollback hard.
  • The headline change re-enables eager bundled plugin dependency installation during package postinstall. That expands install-time network/script execution surface and works against the staged/lazy runtime-deps direction used by the current release-line fixes.
  • The actual 2026.4.22 package was smoke-checked: doctor --fix repairs missing deps and gateway run reaches ready with configured Feishu/Telegram despite initially missing staged deps.
  • No open .22 report was found for the same Cannot find module ... bundled-deps failure shape.

If any of the unrelated fixes in this PR are still needed, please split them into focused PRs with one behavior change each. If there is a real 2026.4.22+ bundled-deps repro, please open a fresh issue/PR with exact version, install method, command, and full require stack so we can patch the current runtime-deps path directly rather than restoring eager postinstall.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling channel: slack Channel integration: slack commands Command implementations scripts Repository scripts size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants