Fix agent mode patching linked first-party source (#626) - #634
Mikola Lysenko (mikolalysenko) wants to merge 8 commits into
Conversation
Assisted-by: Claude Code:claude-opus-5-5
Agent-mode apply and rollback wrote through any node_modules entry, including a link to an npm/yarn/pnpm/bun workspace member, a file: or link: directory dependency, or an npm link target. When that local package shared a patched package's name and version, the default mismatch policy replaced the user's own source with upstream bytes, and rollback then wrote the upstream original over it. A node_modules entry whose real path is outside every node_modules tree is now refused (dry run included), the same way a link into a store shared with other projects already is. Store links (.pnpm, .store, .vlt, .bun) and real directories are patched as before. Fixes #626 Assisted-by: Claude Code:claude-opus-5-5
End-to-end check through the real binary: a node_modules link to an npm workspace member that shares a patched package's name@version is refused by apply, apply --dry-run and rollback, and the member's own source is never overwritten. Refs #626 Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
The earlier commits ran cargo fmt over the whole workspace, which reformatted about 130 files that main carries unformatted. Restore those files so the change only touches the fix, its tests and docs. Refs #626 Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
|
Burn-down agent: Ready for review at
Generated by Claude Code |
|
Codex review of The author’s latest commits address all four reproduced cases: relocated Yarn pnpm registry copies remain patchable, unused Yarn configuration no longer admits the npm workspace overwrite, peer-instantiated registry packages are accepted, and copied tarball dependencies are accepted. The exception remains limited to the documented Yarn configuration and installed-entry forms; source-link protocols stay refused. Verified on a clean, unchanged checkout of this exact commit: 126 focused tests and 28 native CLI operations passed, using native Yarn 4.12.0, npm 11.19.0 and Node 24.21.0 installations. Apply/rollback, including dry runs, work for ordinary, peer and scoped tarball copies. npm and Yarn first-party workspace refusals preserve the source bytes; copied-tarball operations preserve the source archive. The fresh binary and all six PR-changed file hashes were checked against this commit. Changed-file formatting and targeted Clippy pass (with the existing macOS Independent final source review found no remaining actionable issue. The author correction is accepted; no additional code change is required. 402 successful checks, 7 skipped, and 9 successful workflows (one additional workflow skipped). Bugbot is clear on this commit; no unresolved review threads or new actionable findings. Ready for review has been restored. GitHub still requires the normal human approval before merge. |
Yarn's pnpm linker with pnpmStoreFolder set (for example to .cache/.store) links node_modules/<name> to <store>/<entry>/package, outside every node_modules tree. Those are installed registry copies, but the first-party-link guard refused them. Read pnpmStoreFolder from the nearest .yarnrc.yml at or above the project and accept exactly <store>/<entry>/package. A store that contains the project is ignored, so the setting cannot re-admit workspace source. Refs #626 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SoWUfwR22TaCBa9tCuJGpU
|
Re the Codex P2 (relocated Yarn pnpm store): confirmed and fixed in 929db7e.
No need for a separate correction from the review agent; this push covers it. Generated by Claude Code |
|
BugBot review Generated by Claude Code |
The relocated-store exception trusted any pnpmStoreFolder setting. An npm workspace with a stray .yarnrc.yml (nodeLinker: node-modules, pnpmStoreFolder: packages) then let apply and rollback overwrite the member linked at node_modules/<name>. Require a yarn.lock, an active nodeLinker: pnpm, and a target that is exactly a registry entry's <store>/<ident>-npm-<version>-<10 hex>/package directory. Refs #626 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SoWUfwR22TaCBa9tCuJGpU
|
Re the Codex follow-up P1 (an inactive Yarn config could admit an npm workspace member): confirmed, and fixed in 3ac1213, built on 929db7e and keeping the
Tests:
Local runs: clippy is clean, the CLI apply suite passes, and the patch/vendor library tests pass except the 4 permission tests that can't pass in this container (it runs as root), as listed in the PR body. Since this push covers the correction, a second one from the review agent shouldn't be needed. A re-review of 3ac1213 is welcome. Generated by Claude Code |
|
BugBot review Generated by Claude Code |
Yarn names a registry package instantiated for its peers by its virtual locator (react-dom-virtual-685e277730), not npm-<version>, so the relocated-store check refused it as linked source. Accept <ident>-virtual-<10 hex> beside <ident>-npm-<version>-<10 hex>. Workspaces never get a store entry under the pnpm linker, so this admits no first-party source. Refs #626 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SoWUfwR22TaCBa9tCuJGpU
|
Re the Codex P2 on 3ac1213 (peer-instantiated packages such as
This push covers the correction, so the separately prepared one shouldn't be needed. A re-review of f419eb5 is welcome. Generated by Claude Code |
|
BugBot review Generated by Claude Code |
|
Codex follow-up coordination on The real CLI refused all four apply/rollback wet/dry operations on this fixture at The correction identifies a configured project-owned installed layout and documents conservative refusal of ambiguous configuration. I will push it after final reconciliation and native verification, then wait for full CI before restoring Ready for review. |
A scoped file: tarball dependency installs as <ident>-file-<hash> in Yarn's pnpm store, and the relocated-store check refused it. Yarn slugs a store entry as <ident>-<protocol>-<hash>, so accept the protocols it installs as a copy (npm, virtual, file, patch, http, https, git, github). Keep refusing workspace, portal and link, which point at source and never get a store entry, and any unknown protocol. Rename the matcher to is_yarn_copy_slug to match. Refs #626 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SoWUfwR22TaCBa9tCuJGpU
|
Follow-up on the edited Codex review of 3ac1213: f419eb5 covered the
If the separately prepared correction covers a layout these tests don't, please name it and I'll add it. Generated by Claude Code |
|
BugBot review Generated by Claude Code |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit cf1b6e9. Configure here.
LLM Description written by Claude Code:claude-opus-5-5
Fixes #626
Summary
Agent-mode
applyandrollbackno longer write through anode_modules/<name>(ornode_modules/@scope/<name>) link whose real path is outside everynode_modulestree. Package managers link that way only to first-party source: an npm, Yarn, pnpm or Bun workspace member, afile:orlink:directory dependency, or annpm linktarget. Before this change, when such a local package shared a patched package'sname@version, the default mismatch policy replaced the user's own code with upstream bytes.vexthen attested the patch, androllbackwrote the upstream original over the fork. Now both commands fail closed, dry run included. The error names the real path and says to patch that source directly, which matches vendored mode'svendor_workspace_memberrefusal.Root cause
The npm crawler accepts any symlink (or Windows junction) in an importer tree as a package copy. That is meant for pnpm/vlt/Yarn store links, but it never checks where the link resolves. The patch engine then commits through whatever path it was given.
Fix
Agent apply and rollback already have one boundary for package dirs they must not write in place:
patch::shared_store. It refuses pnpm's global virtual store and PDM's symlink cache, and it checks the package dir and every patched file's parent, dry run included. This PR adds a third kind there,SharedStoreKind::LinkedSource, which matches when:node_modulesentry (node_modules/<name>ornode_modules/@scope/<name>), andnode_modules(a real dir, or a link into its own.pnpm/.store/.vlt/.bunstore, even whennode_modulesitself is a symlink) nor below any othernode_modules(a workspace member's link into the root.pnpm).Store links and real dirs are patched exactly as before. That includes Yarn's pnpm-linker store relocated outside
node_modulesviapnpmStoreFolder, but only for an active Yarn pnpm install (ayarn.lockplusnodeLinker: pnpm), only to the<store>/<entry>/packagedir of a package Yarn installs as a copy (npm,virtual,file,patch,http(s), git; neverworkspace/portal/link), and only when the store does not contain the project. Codex review rounds drove this: 929db7e added the exception, 3ac1213 closed a stray-config hole that would have let an npm workspace member through, f419eb5 accepts peer-instantiated (virtual:) entries such asreact-dom, and cf1b6e9 accepts the other copy protocols, such as a scopedfile:tarball. Docs:docs/ecosystems.mddescribes the new refusal next to the shared-store one. Hosted and vendored modes, and the npm/PyPI/gem wrappers, are unaffected (they don't use this path).I chose #626 over the two-issue #628/#629 cluster in the same tier: #626 is silent loss of committed first-party code that no reinstall restores, while #628/#629 is diff noise plus a refactor.
Test evidence
Each test below was run red with the detection disabled (
linked_source_ofreturningNone) and green with the fix:patch::shared_store::tests::node_modules_link_to_first_party_source_is_refused: workspace member, scoped member, annpm linktarget outside the project, and a global-prefixnpm link. Red → green.patch::apply::tests::test_apply_refuses_node_modules_link_to_first_party_source: member, scoped member and out-of-project link, for policies Warn and Force, dry run and real. The fork keeps its bytes. Red → green.patch::rollback::tests::test_rollback_refuses_node_modules_link_to_first_party_source: a fork left patched by a pre-fix apply is not overwritten with the upstream original. Red → green.applysuitein_process_npm_multicopy::apply_and_rollback_refuse_a_node_modules_link_to_first_party_source: the real binary on an npm-workspace tree;apply --dry-run,applyandrollbackexit non-zero, the JSON envelope names the cause, and the member's source is untouched. Red → green.relocated_yarn_pnpm_store_is_not_refused: a native relocated store stays patchable. Refused without ayarn.lock, withnodeLinker: node-modules, withpnpmStoreFolder: ., for a non-packagestore link and for a workspace-slug entry.react-dom-virtual-…and scoped@acme-tool-file-…entries stay patchable. Red → green.stray_yarn_store_setting_does_not_admit_an_npm_workspace_member: the review's reproduction. An npm workspace with a stray.yarnrc.yml(pnpmStoreFolder: packages) linksnode_modules/left-padtopackages/foo/package, and the link stays refused. Red → green.node_modules_store_links_and_real_dirs_are_not_refused: real dirs, scoped real dirs, a member link into the root.pnpm, Yarn's.store/<entry>/package, and a symlinkednode_modulesroot with both real and.storeentries. All stay patchable. Existingtest_apply_patches_through_per_project_pnpm_link, the vlt/npm multi-copy suites and the shared-store tests still pass.Local runs:
mainitself is not fmt-clean under the pinned toolchain, so the reformatting an earlier commit swept in was reverted in 2feeaa5 to keep this PR scoped.cargo clippy --workspace --all-features -- -D warnings: clean.cargo test --workspace --all-features --no-fail-fast: all pass except tests that can't run in this container. These fail identically onmain's versions of the touched files. They are thechmod 0o555write-failure tests (the container runs as root, which ignores the mode):covgap_commands_vendor::*state_write_failure*,in_process_redirectwrite-failure tests, repair cleanup-failure tests,copy_tree::relax_loop_must_not_traverse_symlinked_root,vlt_heal::an_unremovable_hidden_lock…andpypi_*::wire_*failure*. The remaining failures were update-fixture,vlt_lockandregistry_fetchtests that hit a full disk mid-run; the core ones pass on re-run. CI (non-root runners) is the authority for those.CI: all checks green on cf1b6e9 (402 passed, 6 skipped by path filters). Bugbot found no issues on any head. Codex re-review of cf1b6e9 accepted all four of its findings as fixed: it ran 126 focused tests and 28 native CLI operations against real Yarn 4.12, npm 11.19 and Node 24 installs.
Per-issue checklist
file:directory dependency (same on-disk shape as a member link): shared_store + apply testsnpm linktarget outside the project, including via the global prefix: shared_store + apply testscanonicalizeresolves junctions); the tests arecfg(unix)like the existing shared-store tests, and Windows CI runs the restFollow-ups
ecosystem_dispatch.rspush_pathliteral-path dedup) and is left for its own fix.🤖 Generated with Claude Code
https://claude.ai/code/session_01SoWUfwR22TaCBa9tCuJGpU