fix(plugins): stage bundled-plugin runtime-dep install outside the plugin root#70852
Conversation
…ugin root When a packaged bundled plugin's `pluginRoot` is used directly as the npm execution cwd, `npm install <specs>` resolves the plugin's own `package.json` as the project manifest and fails with `EUNSUPPORTEDPROTOCOL: Unsupported URL Type "workspace:": workspace:*` whenever that manifest declares a `workspace:` runtime dep (e.g. `"@openclaw/plugin-sdk": "workspace:*"`). This takes out every plugin with any runtime deps at gateway startup. `ensureBundledPluginRuntimeDeps` already filters `workspace:` specs from the CLI arguments, but npm's own resolver reads the cwd manifest regardless, so the filter alone is not enough. The existing isolated execution-root + `replaceNodeModulesDir` machinery handles this exact problem for source-checkout + cache-hit installs. This change activates the same staging path for the packaged case: when `installRoot === pluginRoot` and we are not in the source-checkout cache path, stage the install inside `<pluginRoot>/.openclaw-install-stage` (which has a minimal generated `package.json`) and move the produced `node_modules/` back to the plugin root as before. - Add regression test `stages plugin-root install when the plugin's own package.json declares workspace:* deps` covering the Docker scenario (mixed `workspace:*` + concrete runtime dep, e.g. anthropic-style `@openclaw/plugin-sdk` + `@anthropic-ai/sdk`). - Update existing plugin-root-install expectations (`installs plugin-local runtime deps when one is missing`, `skips workspace-only runtime deps before npm install`, `installs deps that are only present in the package root`, `does not trust runtime deps that only resolve from the package root`, `does not treat sibling extension runtime deps as satisfying a plugin`) to assert the new `installExecutionRoot`. Reported in openclaw#70844; same root cause as openclaw#70701, openclaw#70756, openclaw#70773, openclaw#70818, openclaw#70839 which see the downstream "Cannot find package 'openclaw' from plugin-runtime-deps" symptom because their `resolveBundledRuntimeDependencyInstallRoot` resolves to an external stage dir (clean manifest) so the install succeeds but the resulting node_modules tree cannot satisfy the filtered-out workspace packages at ESM import time. ## AI assistance This PR was AI-assisted with Claude Code. Testing degree: fully tested for the touched `bundled-runtime-deps` install staging surface. - `pnpm exec vitest run --config test/vitest/vitest.plugins.config.ts src/plugins/bundled-runtime-deps.test.ts` (31/31) - `pnpm exec vitest run --config test/vitest/vitest.plugins.config.ts src/plugins/` (43/43 across 8 files) - `pnpm exec tsgo --noEmit -p tsconfig.core.json`, `pnpm exec tsgo --noEmit -p tsconfig.core.test.json` (clean) - `pnpm exec oxlint src/plugins/bundled-runtime-deps.ts src/plugins/bundled-runtime-deps.test.ts` (0 warnings, 0 errors) - `node scripts/check-src-extension-import-boundary.mjs --json` and `node scripts/check-sdk-package-extension-import-boundary.mjs --json` (both `[]`) I understand the code path changed here: packaged bundled plugins now stage their runtime-dep install one directory below `pluginRoot` so npm never reads the plugin's `workspace:*`-containing manifest during install; after install completes, the produced `node_modules/` is moved back to `pluginRoot` via the existing `replaceNodeModulesDir` helper. Signed-off-by: Simone Macario <[email protected]>
Greptile SummaryThis PR fixes the
Confidence Score: 3/5Fix is functionally correct but leaves a persistent duplicate node_modules tree in the stage directory for every plugin that undergoes a runtime-dep install. The core isolation mechanism works and passes all 31 tests. However, the missing cleanup of src/plugins/bundled-runtime-deps.ts — specifically the post-move cleanup path in installBundledRuntimeDeps (lines 862–868)
|
|
Codex review: this is the right canonical fix for the Discord/Telegram/ACP packaged runtime-dep duplicates I just de-duped into #70844. One merge blocker remains: CI expect(path.resolve(calls[0]!.installExecutionRoot!)).not.toEqual(path.resolve(pluginRoot));
The important part to keep in the PR: packaged plugin-root installs must run from |
|
Codex follow-up pushed on top of the PR. What changed:
Validation I ran locally:
Remote CI is queued/running on the latest SHA now. |
|
Landed via rebase onto main.
Thanks @simonemacario! |
Summary
Fixes gateway-startup plugin-runtime-dep install failure on packaged (Docker image, npm global) installs of 2026.4.22 where every bundled plugin whose
package.jsonstill contains aworkspace:*dep fails with:Reported in #70844 (Docker image,
EUNSUPPORTEDPROTOCOLat install step); same root cause as #70701, #70756, #70773, #70818, #70839 (npm global, downstreamCannot find package 'openclaw' from plugin-runtime-deps/…at ESM import).Root cause
ensureBundledPluginRuntimeDepsinsrc/plugins/bundled-runtime-deps.tsalready filtersworkspace:specs from the CLI argument list (normalizeInstallableRuntimeDepVersion,parseInstallableRuntimeDep). But the install step isspawnSync("npm", ["install", ...specs], { cwd: installExecutionRoot }), andinstallExecutionRootdefaults toinstallRoot. WheninstallRoot === pluginRoot(the packaged case where the plugin dir is writable and no external stage is configured), npm reads the plugin's ownpackage.jsonas the project manifest and tries to resolve itsworkspace:*deps — failing the whole batch regardless of what was passed on the CLI.Evidence:
upstream/maincarriesworkspace:*declarations in 106extensions/*/package.json(predominantly"@openclaw/plugin-sdk": "workspace:*"). The 18 plugins that fail in the Docker repro are exactly the subset that declare such entries as runtime deps (dependencies/optionalDependencies).The npm-global variants in #70701 / #70756 / #70773 / #70818 / #70839 hit a related but different surface:
resolveBundledRuntimeDependencyInstallRootresolves to an external stage dir (clean manifest), sonpm installsucceeds but the resultingnode_modules/tree does not satisfy the filtered-out workspace packages, and the subsequent ESM import ofopenclawfails at plugin load. Same defect class, different failure point.Fix
Activate the existing isolated-execution-root +
replaceNodeModulesDirmachinery for the packaged case too. WheninstallRoot === pluginRootand we are not in the source-checkout cache-dir path, stage the install inside<installRoot>/.openclaw-install-stage(minimal generatedpackage.json) and move the producednode_modules/back to the plugin root as before.No change to the existing
workspace:filter, no new surface, no change to external stage or source-checkout semantics.Shape note
@steipete flagged in #70138 that runtime-dep repair belongs at gateway/plugin startup (not
npm packpostinstall). This change keeps that contract — it only widens when the existing stage/move is activated; the repair still runs throughensureBundledPluginRuntimeDepscalled fromsrc/plugins/loader.ts:2190. It does not reintroduce the eager-install shape that was closed in #70650.Validation
pnpm exec vitest run --config test/vitest/vitest.plugins.config.ts src/plugins/bundled-runtime-deps.test.ts— 31/31 (includes 1 new regression test: stages plugin-root install when the plugin's own package.json declares workspace:* deps)pnpm exec vitest run --config test/vitest/vitest.plugins.config.ts src/plugins/— 43/43 across 8 test filespnpm exec tsgo --noEmit -p tsconfig.core.json— cleanpnpm exec tsgo --noEmit -p tsconfig.core.test.json— cleanpnpm exec oxlint src/plugins/bundled-runtime-deps.ts src/plugins/bundled-runtime-deps.test.ts— 0 warnings, 0 errorsnode scripts/check-src-extension-import-boundary.mjs --json—[]node scripts/check-sdk-package-extension-import-boundary.mjs --json—[]I did not run
codex review --base origin/mainbecause I do not have Codex CLI access locally; happy to run Codex review in a later iteration if a maintainer provides guidance.Test plan
ghcr.io/openclaw/openclaw:<this-branch>and verify gateway startup against an existing~/.openclawthat was pre-populated from 2026.4.15, reproducing the repro in [Bug]: v2026.4.22 official Docker image — 18 bundled plugins fail runtime-dep install with EUNSUPPORTEDPROTOCOL: "workspace:*" #70844 from a clean state.<pluginRoot>/node_modules/is populated after gateway start and the.openclaw-install-stage/sub-directory is cleaned up / idempotent across restarts.pnpm devin the monorepo) still uses the cache-dir stage (not.openclaw-install-stage/) so no regression on contributor ergonomics.Cannot find package 'openclaw'symptom also resolves.AI assistance
This PR was AI-assisted with Claude Code (Opus 4.7). All code paths changed here were read end-to-end by me; the diagnosis against
src/plugins/loader.ts:2190andsrc/plugins/bundled-runtime-deps.tsis documented in the linked issue #70844 comment thread.Testing degree: fully tested for the touched bundled-plugin runtime-dep install-staging surface.
I understand the code path changed here: packaged bundled plugins now stage their runtime-dep install one directory below
pluginRootso npm never reads the plugin'sworkspace:*-containing manifest during install; the existingreplaceNodeModulesDirhelper moves the producednode_modules/back topluginRootafter install succeeds.Closes #70844