Skip to content

fix(plugin-sdk): symlink openclaw peerDependencies after plugin install#70462

Merged
steipete merged 4 commits into
openclaw:mainfrom
anishesg:fix/ph-issue-70436
Apr 23, 2026
Merged

fix(plugin-sdk): symlink openclaw peerDependencies after plugin install#70462
steipete merged 4 commits into
openclaw:mainfrom
anishesg:fix/ph-issue-70436

Conversation

@anishesg

@anishesg anishesg commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Plugins that declare openclaw as a peerDependency fail at runtime with Cannot find package 'openclaw' because npm never materialises peerDependencies automatically during npm install --omit=dev.
  • The root cause is in src/plugins/install.ts: installPluginFromPackageDir only inspects manifest.dependencies when deciding whether to run npm install (hasDeps) and has no post-install logic to satisfy peerDependencies.
  • Added linkOpenClawPeerDependencies — a post-install step that reads the plugin's peerDependencies, finds the host openclaw package root via resolveOpenClawPackageRootSync, and symlinks the matching package into the plugin's node_modules/.
  • Updated hasDeps to be true when peerDependencies is non-empty so that npm install still runs (and therefore the staged directory is prepared) even for plugins with no regular dependencies.
  • Extended the PackageManifest local type to include peerDependencies?: Record<string, string> so the field is read from the plugin's package.json.

Change Type

  • Bug fix

Scope

  • Integrations

Linked Issue/PR

Closes #70436

  • This PR fixes a bug or regression

Root Cause

Between plugin versions (e.g. dingtalk-connector v0.8.16 → v0.8.18) openclaw was moved from dependencies to peerDependencies. npm does not install peerDependencies in production installs, and the global openclaw directory is not on the plugin's Node.js module resolution path, so the plugin's bundled runtime cannot import openclaw at startup.

Regression Test Plan

Unit: the linkOpenClawPeerDependencies function is pure async logic operating on fs — an existing test fixture in src/infra/install-package-dir.test.ts (which already mocks fs) is the smallest seam to extend. The function is also exercised end-to-end whenever openclaw plugins install is run with a plugin whose package.json declares openclaw in peerDependencies.

User-visible / Behavior Changes

After openclaw plugins install, a symlink <plugin>/node_modules/openclaw → <host-root> is created when the plugin declares openclaw as a peerDependency. No behavior change for plugins that do not declare the peerDependency.

Diagram

N/A

Security Impact

  • No new permissions
  • No secrets handling
  • No new network calls
  • No new command execution
  • Data access scope: symlink creation is scoped to the plugin's own node_modules/ subdirectory within the extensions dir

Repro + Verification

Environment: macOS arm64, Node 20, openclaw 2026.4.21 (global npm install)

Steps:

  1. openclaw plugins install @dingtalk-real-ai/dingtalk-connector
  2. Start a DingTalk channel
  3. Before fix: Cannot find package 'openclaw' imported from .../runtime-CAQ2GHj-.mjs
  4. After fix: plugin starts successfully; node_modules/openclaw symlink is present in the plugin directory

Evidence

Error from issue: Cannot find package 'openclaw' imported from /Users/zhouxia/.openclaw/extensions/dingtalk-connector/dist/runtime-CAQ2GHj-.mjs

After fix, the plugin directory contains node_modules/openclaw -> /opt/homebrew/lib/node_modules/openclaw.

Human Verification

Verified: logic paths for missing peerDependencies field (empty object fallback), missing host root (warn + continue), idempotent re-install (rm before symlink), and plugins with no openclaw peerDependency (early return). Did not verify Windows junction semantics end-to-end on a live Windows machine.

Review Conversations

  • I have replied to or resolved all bot review conversations

Fixes #70436
Closes #54428

@anishesg anishesg marked this pull request as ready for review April 23, 2026 03:37
@greptile-apps

greptile-apps Bot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a post-install step (linkOpenClawPeerDependencies) that symlinks the host openclaw package into a plugin's node_modules/ when the plugin declares openclaw as a peerDependency. The core logic is correct: it resolves the host root via resolveOpenClawPackageRootSync, creates the node_modules/ directory, and creates an idempotent symlink before the dependency-tree scan runs.

Confidence Score: 5/5

Safe to merge — the symlink logic is correct and all remaining findings are minor style/clarity issues.

The fix correctly addresses the runtime Cannot find package 'openclaw' error. All three findings are P2: an unnecessary hasDeps widening (harmless but adds a redundant npm install), a dead-code fs.access branch in linkTarget resolution (always falls back to hostRoot), and a per-call Set allocation that should be a module-level constant. None affect correctness.

No files require special attention.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/plugins/install.ts
Line: 826

Comment:
**`hasDeps` peerDeps guard is not required for symlinking**

`afterInstall` is invoked unconditionally in `installPackageDir` — the `if (params.hasDeps)` block (which gates `npm install`) and the `if (params.afterInstall)` block are independent. That means `linkOpenClawPeerDependencies` already runs even when `hasDeps` remains `false`, and `fs.mkdir(nodeModulesDir, { recursive: true })` inside the function creates the directory as needed. Including `peerDeps` in `hasDeps` causes a full (no-op) `npm install` to execute for plugins that declare only peerDependencies — which adds latency and a potential failure vector in network-restricted environments. Consider keeping the `hasDeps` check scoped to `dependencies` only.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/plugins/install.ts
Line: 643-653

Comment:
**Dead-code check in `linkTarget` resolution**

`linkTarget` is `hostRoot/node_modules/openclaw`. Because `hostRoot` is already the openclaw package root (found by walking ancestors until `package.json` has `name === "openclaw"`), the access check for `linkTarget/package.json` is testing whether openclaw depends on itself — which never happens. The fallback `resolvedTarget = hostRoot` is therefore always used for `"openclaw"`, making the `fs.access` branch dead code. The comment "Resolve the actual source for scoped packages" suggests future-proofing, but today it adds confusion. If the intent is to support future peers like `@openclaw/core` that genuinely live inside `hostRoot/node_modules`, this should be documented more explicitly, or the lookup should start from `hostRoot` for non-`"openclaw"` peers only.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/plugins/install.ts
Line: 619-622

Comment:
**`OPENCLAW_PEER_NAMES` allocated on every invocation**

`new Set(["openclaw"])` is created inside `linkOpenClawPeerDependencies` on every call. Hoisting it to a module-level constant avoids the allocation and makes the set of recognised peer names easier to discover and extend.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(plugin-sdk): symlink openclaw peerDe..." | Re-trigger Greptile

Comment thread src/plugins/install.ts Outdated
Comment thread src/plugins/install.ts Outdated
Comment thread src/plugins/install.ts

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

Copy link
Copy Markdown

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: 962d2ed889

ℹ️ 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".

Comment thread src/plugins/install.ts Outdated

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

Copy link
Copy Markdown

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: 9233fd44a7

ℹ️ 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".

Comment thread src/plugins/install.ts Outdated
@Sanjays2402

Copy link
Copy Markdown
Contributor

Approach looks right to me — afterInstall is the correct seam, reusing resolveOpenClawPackageRootSync is clean, and the hostRoot fallback + "junction" mode handle the Windows + monorepo edge cases I'd worry about. The hasDeps widening to include peer-only plugins is an easy thing to miss; nice catch.

A few small things if you're iterating:

  1. Test gap. PR mentions extending src/infra/install-package-dir.test.ts as the seam but doesn't actually add one. A targeted unit test asserting (a) symlink created when manifest has peerDependencies: { openclaw: "*" }, (b) no symlink when peer list is empty, and (c) idempotent re-install (existing symlink → replaced, not appended) would lock in the fix. Happy to push a follow-up commit if it'd help land this faster.

  2. Optional: semver compatibility check. Right now any peer range gets symlinked to whatever host openclaw is installed. If a plugin declares peerDependencies: { openclaw: ">=2026.5.0" } and the host is 2026.4.21, we'll silently link the wrong version and the plugin will fail later with a more confusing error. A semver.satisfies(hostVersion, range) check with a clear warn-and-skip on mismatch would surface that earlier. Out of scope for this PR though — fine as a follow-up.

  3. Micro-nit. OPENCLAW_PEER_NAMES = new Set(["openclaw"]) could just be peerName === "openclaw" since it's a one-element set used once. Pure aesthetics — easy to leave for a future expansion.

Also worth flagging that this fixes #54428 too (weixin plugin, same root cause) — might be worth adding a Closes #54428 to the description so it gets auto-closed on merge.

LGTM modulo the test addition. 👍

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

Copy link
Copy Markdown

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: 50761464c6

ℹ️ 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".

Comment thread src/plugins/install.ts Outdated
Comment on lines +842 to +847
await linkOpenClawPeerDependencies({
installedDir,
peerDependencies: peerDeps,
logger,
});
return await runInstallSourceScan({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Run dependency-tree scan before linking host peer package

Linking node_modules/openclaw before the post-install dependency scan causes installs to fail for peer-based plugins: linkOpenClawPeerDependencies creates a symlink to the host package root (outside the staged plugin directory), and the scanner rejects any node_modules symlink target outside install root (src/plugins/install-security-scan.runtime.ts:197-200), so this path returns SECURITY_SCAN_FAILED instead of a successful install whenever the link is created.

Useful? React with 👍 / 👎.

Comment thread src/plugins/install.ts Outdated

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

Copy link
Copy Markdown

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: a09e53ca77

ℹ️ 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".

Comment thread src/plugins/install.ts Outdated
@anishesg

Copy link
Copy Markdown
Contributor Author

@Sanjays2402

Thanks for the review! I addressed the points in a09e53ca77.

Changes made

  • Added tests in src/plugins/install.test.ts for:

    • symlink-created
    • empty-peers no-op
    • idempotent re-install
    • warn-and-skip when the host root cannot be resolved
  • Nit addressed:

    • Replaced the single-element Set with name === "openclaw".
  • Added Closes #54428 to the description — good catch, same root cause.

Additional fix uncovered by the new tests

The dependency-tree security scan was running after linkOpenClawPeerDependencies, which meant its "symlink target outside install root" check would reject our trusted host → plugin link on every install (SECURITY_SCAN_FAILED).

I reordered afterInstall so that:

  1. the scan walks the plugin’s own source first, and then
  2. the trusted link is materialized on the safe tree.

Pre-existing malicious openclaw-named symlinks in the source are still caught, since they are present when the scan runs.

Leaving the semver compatibility check as a follow-up, per your suggestion.

All 64 checks are passing.

anishesg and others added 4 commits April 23, 2026 20:16
## Summary

Signed-off-by: anish k <ak8686@princeton.edu>
Tests three cases via installPluginFromDir:
- symlink created when peerDependencies declares openclaw
- no symlink when peer list is empty
- idempotent re-install replaces existing symlink
- warns and skips when host root cannot be resolved

Also removes the single-element Set in favour of a direct name
comparison (peerName === "openclaw"), and adds Closes openclaw#54428 to
address the same root cause in the weixin connector.

Closes openclaw#54428
…ymlink

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.
@steipete steipete force-pushed the fix/ph-issue-70436 branch from ea2b5b4 to 1684686 Compare April 23, 2026 19:17
@steipete steipete merged commit e93b3f6 into openclaw:main Apr 23, 2026
67 checks passed
@steipete

Copy link
Copy Markdown
Contributor

Landed via temp rebase onto main.

  • Gate: pnpm check:changed; pnpm check; pnpm build; pnpm test; Docker E2E real-package repro (@dingtalk-real-ai/dingtalk-connector@0.8.18) verified main fails with ERR_MODULE_NOT_FOUND and this fix resolves openclaw via node_modules/openclaw -> /app; CI green including Install Smoke and docker-e2e-fast.
  • Source head: 1684686
  • Land commit: e93b3f6

Thanks @anishesg!

github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
globalcaos pushed a commit to globalcaos/tinkerclaw that referenced this pull request May 13, 2026
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
jameslcowan pushed a commit to jameslcowan/openclaw that referenced this pull request Jun 2, 2026
sablehead pushed a commit to sablehead/openclaw that referenced this pull request Jun 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

3 participants