fix(plugins): prevent openclaw update from silently downgrading managed deps#85305
fix(plugins): prevent openclaw update from silently downgrading managed deps#85305alkor2000 wants to merge 4 commits into
Conversation
|
Codex review: needs real behavior proof before merge. Reviewed May 27, 2026, 4:54 AM ET / 08:54 UTC. Summary PR surface: Source +77, Tests +201. Total +278 across 4 files. Reproducibility: yes. at source level: current main unconditionally upserts the managed dependency spec before npm install, and the linked report gives a concrete update path where that rewrites a newer installed direct dependency to an older bundled version. I did not run a mutating live update in this read-only review. Review metrics: 1 noteworthy metric.
Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Proof guidance: Risk before merge
Maintainer options:
Next step before merge Security Review detailsBest possible solution: Land the narrow downgrade guard after live update proof and clean required checks, leaving the broader respect-all-pins policy to a separate maintainer decision. Do we have a high-confidence way to reproduce the issue? Yes at source level: current main unconditionally upserts the managed dependency spec before npm install, and the linked report gives a concrete update path where that rewrites a newer installed direct dependency to an older bundled version. I did not run a mutating live update in this read-only review. Is this the best way to solve the issue? Yes for the narrow no-silent-downgrade case: the PR guards only comparable update downgrades, verifies against the preserved version, and returns matching metadata. The remaining gap is proof, not a different code shape; broader pin-respect policy should stay separate. AGENTS.md: found and applied where relevant. Codex review notes: model gpt-5.5, reasoning high; reviewed against 527b7c2eed2f. Label changesLabel justifications:
Evidence reviewedPR surface: Source +77, Tests +201. Total +278 across 4 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
|
ClawSweeper PR egg 🎁 Pass real behavior proof to wake the egg and unlock a hatchable treat. Where did the egg go?
|
|
@clawsweeper re-review Addressed the [P1] (make the preserve branch pass install verification, install.ts:715 area):
|
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
|
@clawsweeper re-review Addressed the [P2] (return the preserved npm resolution, install.ts:917-926):
|
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
bf7c4f5 to
e9b6e34
Compare
…ed deps Fixes openclaw#85184. `openclaw update` re-syncs externalized bundled-plugin direct dependencies through installPluginFromManagedNpmRoot, which unconditionally wrote the bundled spec into the managed npm root and reinstalled it. When a user had a newer version pinned/installed (e.g. a deliberately applied hotfix), the update silently downgraded it with no warning and no opt-out. Add a narrow guard: during `update`, before writing the managed npm root dependency, compare the version recorded in the managed lockfile against the version the update wants to install. If the incoming version is older (a downgrade) and both versions are concrete and comparable, keep the installed version and emit a warning instead of overwriting it. The same guard is applied to the alias-override retry path so the downgrade cannot slip through there either. Scope is intentionally narrow: only the unambiguous downgrade case during update is guarded. Non-downgrade pin rewrites and the broader respect-all-pins policy are left to maintainer decision. Install mode, fresh installs, and unparseable/range specs preserve the original behavior. Adds focused unit tests for the downgrade-detection logic.
Follow-up within openclaw#85184: the downgrade guard skipped the dependency rewrite, but the post-install verification still compared the on-disk version against the older incoming npmResolution, so a preserved newer dependency was misread as a failed install and rolled back (removing the plugin). Verify against the preserved installed version when the guard fires, so the intentional skip completes instead of triggering rollback. Add end-to-end integration tests through installPluginFromNpmSpec covering both the preserve path (update would downgrade, keep newer, no rollback) and the normal upgrade path (guard does not fire).
Follow-up within openclaw#85184: when the downgrade guard preserves a newer installed dependency, also return the preserved npm resolution rather than the older incoming one, so install records and config metadata match the version actually present in the managed npm root. Extends the integration test to assert the returned resolution reflects the preserved version.
e9b6e34 to
51b946a
Compare
…ng stale resolution Addresses ClawSweeper [P1]/[P2] on openclaw#85305. [P1] readManagedNpmRootInstalledDependency read package-lock.json with the strict readJson, so a fresh managed npm root (no lockfile yet) threw instead of returning null. The downgrade guard reads installed metadata before npm install creates the lockfile, so a fresh plugin install crashed. Switch to the ENOENT-safe readJsonIfExists (already used elsewhere in this file for the same lockfile), so a missing lockfile yields null (no installed dependency). [P2] When the guard preserved a newer installed dependency, the returned expectedNpmResolution spread the older incoming resolution and only overwrote version/integrity, leaving stale resolvedSpec/shasum/resolvedAt from the rejected downgrade target. Construct a clean resolution carrying only the kept version's name/version/integrity so persisted install records do not mix metadata. Tests: fresh-root reader test (returns null without a lockfile) and a preserved-resolution assertion (no stale shasum/resolvedSpec).
|
@clawsweeper re-review Addressed both review findings in commit 9758a00:
Before/after proof for both findings is in the PR body. |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
|
Closing this PR. After refreshing against current The underlying issue (#85184 — If a maintainer still wants this addressed, it would be cleaner to implement fresh against the current install path rather than salvage this branch. Happy to do that if it's wanted — otherwise leaving #85184 open for whoever picks it up. Thanks to the reviewers for the detailed feedback along the way. |
Summary
Fixes #85184. Prevents
openclaw updatefrom silently downgrading a newer already-installed managed direct dependency, and verifies the preserved version through the full install path so the intentional skip does not trigger a rollback.What changed
update, if the incoming version is older than the version already recorded in the managed npm root, keep the installed version instead of overwriting it.update. Non-downgrade pin rewrites and the broader respect-all-pins policy are left to maintainer decision.Real behavior proof
Behavior or issue addressed:
openclaw updatere-synced externalized bundled-plugin direct dependencies through installPluginFromManagedNpmRoot, which overwrote the managed npm root dependency and could silently downgrade a newer pinned/installed version. An earlier guard alone was insufficient: the post-install verification still compared the on-disk version against the older incoming resolution, so a preserved newer dependency was rolled back. This change guards the downgrade and aligns the verification with the preserved version.Real environment tested: Linux (Ubuntu 24.04, WSL2), Node.js 24.14.0, current PR head. Exercised the full installPluginFromNpmSpec update path with vitest, plus focused unit tests for the predicate.
Exact steps or command run after this patch: ran the plugin install/update test suite under the real runtime via vitest.
Evidence after fix: stdout from the test run against the real runtime:
Observed result after fix: The integration test drives the full installPluginFromNpmSpec update path. With an installed 0.11.2 and an update resolving 0.10.0, the run completes successfully (no rollback) and the managed package.json keeps 0.11.2. A normal upgrade (installed 0.11.0, update 0.11.2) still rewrites to the newer version. All managed-root and update-cli acceptance suites pass unchanged.
What was not tested: A live mutating
openclaw update --tagagainst a real registry was not run; the install/update path is exercised end to end with vitest using the real runtime and a mocked npm transport, matching the source-level reproduction in the issue.Follow-up: addressed ClawSweeper [P1]/[P2] (commit 9758a00)
[P1] Fresh-install crash — ENOENT-safe lockfile read
readManagedNpmRootInstalledDependencyreadpackage-lock.jsonwith the strictreadJson, which throws on a missing file. The downgrade guard calls it beforenpm installcreates the lockfile, so a fresh managed root (no lockfile yet)threw instead of returning null, crashing a fresh plugin install. Fixed by using
readJsonIfExists— already used a few lines above in the same file for the samelockfile — so a missing lockfile yields
null(no installed dependency).Real environment tested: Linux (Ubuntu 24.04, WSL2), Node.js 24.14.0, PR head 9758a00.
Command:
node scripts/run-vitest.mjs run src/infra/npm-managed-root.test.ts -t "fresh managed root"Before (strict
readJson, the pre-fix state): the fresh-root reader test fails — the reader throws on the absent lockfile:× returns null for a fresh managed root without a package-lock.json
AssertionError: promise rejected "JsonFileReadError: Failed to read JSON fi…" instead of resolving
Caused by: JsonFileReadError: Failed to read JSON file: /tmp/openclaw-npm-managed-root-XXXX/package-lock.json
❯ readManagedNpmRootInstalledDependency src/infra/npm-managed-root.ts:1005
Caused by: Error: File not found: .../package-lock.json
Serialized Error: { code: 'ENOENT' }
Tests 1 failed | 18 skipped (19)
After (
readJsonIfExists, this PR):✓ returns null for a fresh managed root without a package-lock.json
Tests 1 passed | 18 skipped (19)
[P2] Preserved resolution no longer leaks stale downgrade-target metadata
When the guard preserved a newer installed dependency,
expectedNpmResolutionspread the older incoming resolution and only overwrote version/integrity,
leaving stale
resolvedSpec/shasum/resolvedAtfrom the rejected downgradetarget, which then persisted into install records. Fixed by constructing a clean
resolution carrying only the kept version's name/version/integrity.
Command:
node scripts/run-vitest.mjs run src/plugins/install.test.ts -t "preserves a newer installed managed dependency without rollback"Before (spread incoming, the pre-fix state): the integration test fails — the
rejected downgrade target's
shasum: "abc"leaks into the preserved resolution:× preserves a newer installed managed dependency without rollback during update (#85184)
AssertionError: expected 'abc' to be undefined
793| expect(result.npmResolution?.shasum).toBeUndefined();
Tests 1 failed | 120 skipped (121)
After (clean construction, this PR):
✓ preserves a newer installed managed dependency without rollback during update (#85184)
Tests 1 passed | 120 skipped (121)
Full suites (PR head 9758a00)
$ node scripts/run-vitest.mjs run src/plugins/install.test.ts src/infra/npm-managed-root.test.ts
Test Files 2 passed (2)
Tests 140 passed (140)
What was not tested: A live mutating
openclaw update --tagagainst a realregistry was not run (offline environment). The fresh-install crash and the
stale-metadata leak are both reproduced and verified as focused before/after
unit/integration tests against the real runtime, matching the source-level
findings.