Fix agent apply writing into shared package stores (#332, #361) - #486
Conversation
Assisted-by: Claude Code:claude-opus-5-5
PDM's symlink install cache and pnpm's global virtual store link a package directory into a store every project on the machine uses. Agent apply renamed the patched file inside that directory, so other projects were patched too, and a rollback in one project silently unpatched the rest. Apply now refuses a package whose real location is such a store, and rollback refuses whenever it would write there. The error names the store and how to get a private copy. Per-project stores reached through a symlink (node_modules/.pnpm, workspace links) are patched as before. Fixes #332, #361. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
A PyPI patch is rooted at site-packages with file keys like urllib3/response.py, so the PDM cache link sits below the root and the first guard, which only looked at the root, never saw it. Apply and rollback now classify the directory of every patched file, so a PDM symlink-cache package is refused as intended. Refs #332. 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 |
|
Reviewed No actionable correctness or security regressions found. Shared-store detection covers the package root and nested file-parent directories, including PyPI site-packages layouts. Apply refuses even already-patched shared copies; rollback permits an already-original no-op and refuses writes. Ordinary per-project store paths retain their existing behavior. Validation: |
|
Final merge check for |
Resolve the CHANGELOG conflict by keeping both Fixed entries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5H2ijb6wThkigkiW7vRr7
|
[agent] Merged 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 3628931. Configure here.
|
[agent] Bugbot reviewed the merge head Generated by Claude Code |
|
Re-reviewed The update merges Fresh validation: |
LLM Description written by Claude Code:claude-opus-5-5
Fixes #332
Fixes #361
Summary
In agent mode,
applyandrollbackno longer write through a package directory that links into a store shared by other projects on the machine. Two package managers install that way: PDM's symlink install cache and pnpm's global virtual store. A package that resolves into one of those stores is now refused, and the error names the store and how to get a private copy.Root cause
Agent-mode apply and rollback commit each file with a stage +
rename(2)in the file's parent directory. That isolates a hardlinked or symlinked file, but not a package directory that is itself a symlink into a store shared by every project on the machine:install.cache = trueandcache_method = symlink:site-packages/<pkg>points to<cache>/packages/<wheel-stem>/lib/<pkg>(Agent mode writes the patch into PDM's shared install cache when PDM 2.0–2.12 installs packages as directory symlinks (install.cache + symlink) #332, in both project and-gmode).enableGlobalVirtualStore):node_modules/<dep>points to<store>/v10|v11/links/…/node_modules/<dep>(Agent-mode apply and rollback on a pnpm project with enableGlobalVirtualStore patch (and unpatch) every other project that shares the store #361).The rename lands inside the shared directory, so another project gets patched, and a rollback in one project unpatches the others.
Fix
A new
patch::shared_storemodule classifies a package path by its real (canonical) location. Detection is positive and marker-based:packages/<stem>/with areferrersfile, and the path under itslib/. Checked against the PDM 2.0.3 and 2.12.4 sources (CachedPackage,install_wheel_with_cache).v<N>/linkswith the store'sfiles/beside it. Checked against a real pnpm 10.28 GVS install.Ordinary pnpm
node_modules/.pnpmlinks, workspace links and relocated per-project virtual stores carry neither marker, so they are patched as before.The check covers every directory the patch writes into (
shared_store_of_patch_dirs): the package root plus the parent of each file key. That matters for PyPI, where the root issite-packagesand the PDM link issite-packages/<pkg>below it (Bugbot finding on b3a739f, fixed in 0f84373).apply_package_patch_atrefuses such a package in every state, dry run and already-patched included, so this project never records the shared copy as its own patch. The pnpm/vlt peer copies run through the same engine and get the same check.rollback_package_patch_atrefuses whenever it would write, dry run included. A shared copy that is already original needs no write and passes.The refusal comes out as
failed/apply_failedwith the message: "Refusing to patch : it is in pnpm's global virtual store (enableGlobalVirtualStore), which is shared by other projects on this machine … set enableGlobalVirtualStore to false and reinstall, or usescan --mode hosted/--mode vendored". The PDM message suggestspdm config install.cache_method hardlinkinstead.CHANGELOG (Unreleased › Fixed) and
docs/ecosystems.mdare updated.Test evidence
patch::apply::tests::test_apply_refuses_shared_store_package_dir(npm leg),test_apply_refuses_already_patched_shared_store_package_dir,patch::rollback::tests::test_rollback_refuses_shared_store_package_dir(npm leg)test_apply_refuses_shared_store_package_dir(pypi leg),test_rollback_refuses_shared_store_package_dir(pypi leg)shared_store::tests::patch_dirs_find_a_link_below_the_package_root, plus the PyPI legs above, which use the realsite-packages+urllib3/index.jsshapetest_apply_patches_through_per_project_pnpm_link,shared_store::tests::*(detection tests incl. negatives)Red run on this branch before the guard:
3 failed; 5 passed. With the first, root-only guard, the PyPI-shaped legs still fail (pkg:pypi/urllib3@1.26.18 dry_run=true: must refuse). With 0f84373, everything passes.Real pnpm 10.28 end-to-end (
enableGlobalVirtualStore: true, projects a/b/c sharing one store, hand-staged.socket/forpkg:npm/is-odd@3.0.1):applyin b:failed/apply_failed("Refusing to patch …/store/v10/links/@/is-odd/3.0.1/…/node_modules/is-odd …"), exit 1. c'sindex.jsis untouched.rollbackin a is refused (exit 1) and the shared bytes stay patched, so b and c aren't silently unpatched.node_modules/is-odd -> .pnpm/…) still applies and rolls back normally.Checks
cargo clippy --workspace --all-features -- -D warnings: clean.cargo test --workspace --all-features --no-fail-fast: everything passes except 13 tests in 4 targets. All 13 are permission tests that make a directory read-only and expect a write to fail, and that can't happen when the sandbox runs as root (e.g.copy_tree::relax_loop_must_not_traverse_symlinked_root, the*_state_write_failure_*/*unremovable*tests). All of them are green in CI.cargo fmt --all -- --check:mainitself isn't rustfmt-clean under the pinned 1.93.1 rustfmt (it reformats 120+ unrelated files), so I ran rustfmt only on the files this PR changes, and they are clean.Follow-ups
pthcache method already fails closed (File not found), andsymlink_individual/hardlinkare isolated by the file-level rename. Neither needs a change.🤖 Generated with Claude Code
Note
Medium Risk
Changes agent-mode patch/rollback behavior for PDM symlink-cache and pnpm GVS installs (now errors instead of cross-project writes); detection logic must not false-positive on normal per-project layouts.
Overview
Agent-mode
applyandrollbacknow fail closed when a target package directory (or any parent dir touched by the patch keys) resolves into a cross-project shared store, instead of writing through a symlinked package root and affecting every linked project.A new
patch::shared_storemodule detects PDM’s symlink install cache (packages/…/referrers+lib/) and pnpm’s global virtual store (v<N>/linksbesidefiles/). Per-project pnpm layouts are unchanged. Refusal messages name the store and suggest remediation (e.g. disable GVS,pdm config install.cache_method hardlink) or hosted/vendored mode. Rollback skips the check when everything is already original (no write). CHANGELOG anddocs/ecosystems.mddocument the behavior; regression tests cover apply/rollback for both ecosystems and PyPIsite-packagesroots.Reviewed by Cursor Bugbot for commit 3628931. Configure here.
Generated by Claude Code