Fix npm store copies missed by agent apply and vex (#601, #603) - #605
Conversation
Assisted-by: Claude Code:claude-opus-5-5
Agent-mode apply and rollback now reach a copy bundled inside another package's pnpm, vlt, Bun or Deno store entry even when the same name@version is installed normally: the resolver probes a skipped store entry's bundled tree (one stat per entry). Agent-mode vex now checks the store peer-variant copies apply patches, so one unpatched copy omits the purl instead of producing a false not_affected. Fixes #601, #603 Assisted-by: Claude Code:claude-opus-5-5
Assisted-by: Claude Code:claude-opus-5-5
Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
|
[burn-down agent] Labeled Ready for review at
Generated by Claude Code |
|
Review updated for The correction bounds repeated npm store scans and bundled-directory cycles while preserving every-copy coverage:
All 228 focused tests passed, including multicopy apply/rollback, agent and hosted VEX, crawler oracles, and the new work-count/cycle regressions. The original cycle reproducer exceeded two seconds and now resolves in about 1.44 ms. The 128-copy expanded-input helper measurement improved from about 6.24 s to 224 ms; these are component timings, not whole-command benchmarks. Deterministic tests establish the store-scan bound. The committed files match the tested source. Independent review found no remaining actionable issue; formatting, diff checks, and targeted clippy passed (with the existing macOS No remaining code finding from this review. The Ready label has been restored after all checks completed on the corrected commit. CI note: the initial Windows Bun 1.2.23 job failed because its patch-detail API call timed out before the expected refusal. Artifact |
|
Confirmed on You've said you're preparing the correction on this branch, so I'm not pushing a competing one. The fix I'd propose expands only those additions and leaves the installed paths as they are: for purl in shared.values().flatten() {
let installed_paths = installed.get(purl).cloned().unwrap_or_default();
let mut paths = all.remove(purl).unwrap_or_default();
paths.extend(aliases.remove(purl).unwrap_or_default());
if npm.contains(&purl) {
// `installed` is already store-variant expanded (#603); expand only
// what the alias walk and identity fallback added here.
let added: Vec<PathBuf> = paths
.into_iter()
.filter(|p| !installed_paths.contains(p))
.collect();
let mut merged = installed_paths;
for p in with_store_peer_variant_copies(added).await {
if !merged.contains(&p) {
merged.push(p);
}
}
paths = merged;
}
// ...
}This keeps every-copy verification for agent records, and aliased or fallback copies still get their variants. For a purl with no alias or fallback copies, the store scan count goes back to one. The existing Generated by Claude Code |
|
Cursor (@cursor) review Both review findings are corrected in |
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 b92456b. Configure here.
|
Update (23:20 UTC): the Bun patch compatibility workflow on It is still unexplained. If Original reportCI on Generated by Claude Code |
Release notes are written when a release is cut, from the merged PR log and the code, so PRs no longer edit CHANGELOG.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Resolves conflicts with main's npm alias (#738), first-party link (#634) and pnpm store (#698) changes: - CLI_CONTRACT.md: keep main's hosted row and this PR's agent row. - vex_consumed.rs: drop aliases the installed lookup already found (main), then store-expand only the new ones (this PR), so already expanded copies are not scanned again. - Tests: keep both sides' new multicopy and e2e_vex regressions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0174mrknEY9ge42c94RNRRBx
main fails commands::vex_consumed::tests::hosted_expands_alias_only_copies and hosted_reuses_expanded_npm_copies_and_merges_alias_variants since #605 landed alongside #738; #851 fixes the tests. Carry the same change so this PR's CI (coverage, test-release) is green; it no-ops once main has it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPHxnE5P1rkfCpHFFCwzFR
main went red when #605 taught the name-keyed resolver to find pnpm store copies, which the #738 alias tests assumed it missed. Same change as #851; it no-ops once main carries it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NEpjVvY7X41jPuVuoiCLVz
* Start fix for #806, #821 Assisted-by: Claude Code:claude-opus-5-5 * Unwind uv vendoring after a relock After vendoring, an ordinary uv relock (`uv add --dev x`, `uv add y`) re-serializes the lock arrays that hold our element: the dev group's requires-dev line and `[manifest] overrides`. Revert matched those arrays by their exact recorded text, so it saw drift and kept uv.lock wired, but still reverted pyproject.toml. The pair then failed `uv sync --locked` while `vendor --revert` reported success. Revert now finds our unchanged element inside the live array under the same key and restores or removes just that element, rendering the array the way uv writes it. A pair gate also writes neither file when any record is genuinely drift-kept, so pyproject.toml and uv.lock always stay consistent. Fixes #806, #821. Assisted-by: Claude Code:claude-opus-5-5 * Test uv revert after a relock with real uv Vendor six, run the uv command that re-serializes the lock array around our element (`uv add --dev zipp` for a dev group, `uv add idna` beside user overrides), then revert. Both files must be unwired with no drift warning, and `uv lock --check` must pass. Refs #806, #821. Assisted-by: Claude Code:claude-opus-5-5 * Document uv revert after a relock Refs #806, #821. Assisted-by: Claude Code:claude-opus-5-5 * Anchor uv array reverts on their key A [manifest] overrides record holds the bare array, and the old convergence shortcut searched the whole lock for it. When the root requires-dist happened to match the user's overrides array, revert treated our element as already gone, left it in uv.lock and deleted the artifact it points at. Every whole-array record now reverts through its own key: an untouched array is restored verbatim, otherwise just our element is. Refs #806. Assisted-by: Claude Code:claude-opus-5-5 * Fail closed when a uv lock array can't be read Revert treated any miss locating a whole-array record as convergence, including a key spelled differently or an unbalanced array. A lock that still routed through the vendored wheel could then lose the wheel. Only a key or section that is provably absent now counts as converged. Anything unreadable is drift, which keeps both files and the artifact. Refs #806, #821. Assisted-by: Claude Code:claude-opus-5-5 * Start fix for #840 Assisted-by: Claude Code:claude-opus-5-5 * Test uv revert after a declaration edit A vendored uv revert writes back the lock specifier it recorded when vendoring. If the user changed the package's requirement in pyproject.toml in the meantime, the lock no longer matches and `uv sync --locked` fails. These tests pin the expected behaviour for requires-dist, requires-dev groups and [manifest] constraints. Refs #840 Assisted-by: Claude Code:claude-opus-5-5 * Re-derive uv specifiers on vendored revert When six is vendored, uv.lock records it as a path source with no version specifier. If the user then changes six's requirement in pyproject.toml (uv add "six>=1.16"), the lock stays byte-identical, and vendor --revert, remove and rollback wrote back the specifier recorded at vendoring time. The revert reported success, but `uv sync --locked` then failed. The revert now writes the specifier pyproject.toml declares now, using the same derivation the hosted unwind uses. This covers requires-dist (each extra separately), requires-dev groups and [manifest] constraints. An unchanged declaration still restores byte-for-byte. When uv's spelling can't be derived, such as a multi-clause range whose clause order varies between uv releases, the revert keeps both files and warns vendor_lock_entry_drifted instead of breaking the lock. Fixes #840 Assisted-by: Claude Code:claude-opus-5-5 * Document uv revert after a declaration edit Refs #840 Assisted-by: Claude Code:claude-opus-5-5 * Adapt uv specifier re-derivation to main #625 on main changed the uv declaration reader to report each optional-dependencies member's extra. The merge of main into this branch no longer compiled. Use that reader for requires-dist instead of the local extras walk. Dev groups now also pick up main's group-name normalization and include-group expansion. Refs #840 Assisted-by: Claude Code:claude-opus-5-5 * Pick the declaration a uv lock entry mirrors When a package is declared twice, for example under two environment markers, or directly and through an include-group, the revert kept the recorded specifier whenever any one declaration still matched it. Edit just one of them and the stale pin came back, with the same broken `uv sync --locked` as #840. The revert now picks the declaration by the entry's own marker, as the hosted unwind does. Declarations that still disagree after that are treated as drift, and both files are kept. Refs #840 Assisted-by: Claude Code:claude-opus-5-5 * Tighten uv revert specifier re-derivation Two cases Bugbot found on the vendored uv revert: - A same-name declaration that isn't a plain version range, such as an extra pinned with ===, stopped every entry from following its edited declaration. Now only the entry whose own declaration is unreadable keeps its recorded spelling. - After a bound was dropped, the restored { name = "six" } element also matched another dependency entry in uv.lock, so a drifted wiring could pass as already reverted. The check now looks only in the root unit's requires-dist array. Refs #840 Assisted-by: Claude Code:claude-opus-5-5 * Format the uv revert re-derivation changes Assisted-by: Claude Code:claude-opus-5-5 * Port #851: fix vex alias tests broken on main main has been red since #605 (4646693). Two vex_consumed tests from #738 assumed the name-keyed copy resolver never returns npm-aliased copies, and #605 taught it to. This ports #851's tests-only fix unchanged so this PR's coverage and macOS test jobs can go green; it no-ops once #851 lands on main. Assisted-by: Claude Code:claude-opus-5-5 --------- Co-authored-by: Claude <noreply@anthropic.com>
* Fix vendored gem rewrite breaking positional args Vendoring a gem declared with a splat, constant or method-call version (`gem "rack", *V`, `gem "rack", VERSION`, `ENV.fetch(...)`) wrote that argument after the new `path:` keyword. Ruby rejects that, so every later `bundle` command failed to parse the Gemfile even though vendor reported success and VEX attested the patch. The exact pin supersedes these constraints, so they are now dropped like quoted ones. Keyword options such as `require: false` and a trailing comment still follow `path:`. A real-bundler e2e checks the rewritten Gemfile installs frozen and loads the vendored copy. Fixes #847 Assisted-by: Claude Code:claude-opus-5-5 * Fix vex alias tests broken by store-copy merge #605 taught the name-keyed npm resolver to probe bundled store trees, so it now finds aliased copies (node_modules/lp) and a nested host's store peers itself. Two vex_consumed tests from #738 assumed that set never held aliases, so main's CI went red after both merged. The tests now feed the alias-free set explicitly to keep covering alias expansion, and also check the resolver's own set reaches the same copies with no duplicates. No production code changes. Assisted-by: Claude Code:claude-opus-5-5 (cherry picked from commit 40dac07) --------- Co-authored-by: Claude <noreply@anthropic.com>
…850) * Start refactor for #823 Assisted-by: Claude Code:claude-opus-5-5 * Spawn CLI test children via one hermetic builder Test children inherited ambient SOCKET_* settings through 15 private scrub_socket_env copies and 10 unscrubbed spawners, so a developer's shell (SOCKET_DRY_RUN, SOCKET_OFFLINE, ...) could silently change what a suite exercises. common/hermetic.rs now holds the one builder: hermetic::command seeds and scrubs SOCKET_* and forces SOCKET_NO_CONFIG and SOCKET_NO_UPDATE_CHECK; scrub_extra adds the opt-in yarn, pnpm and venv sweeps. run_bin_with_env is built on it. This moves the 8 copies and 8 unscrubbed spawners that no open fix PR touches onto the builder and deletes those copies. spawn_env_hygiene tests the builder's contract and ratchets the remaining copies and bare binary spawns. Test-only; no production change. Refs #823 Assisted-by: Claude Code:claude-opus-5-5 * Drop imports the hermetic move left unused Assisted-by: Claude Code:claude-opus-5-5 * Spawn cli_dry_run_paths through the hermetic builder Ambient SOCKET_DRY_RUN failed the real-apply leg of apply_dry_run_with_real_patch_verifies_without_mutating; the cli target now gives the same result with or without it. Refs #823 Assisted-by: Claude Code:claude-opus-5-5 * Port #851 vex alias test fix from main breakage main @ 4646693 (#605) broke two vex_consumed alias tests; the coverage job fails on every PR. Same change as #851, so it no-ops once that lands. Assisted-by: Claude Code:claude-opus-5-5 --------- Co-authored-by: Claude <noreply@anthropic.com>
Main is red since #605 (4646693): two commands::vex_consumed tests assume the name-keyed resolver never returns npm-aliased copies, and #605 taught it to find them. This fails socket-patch-cli --lib in coverage and test on every PR. Port #851's test-only fix so this PR can go green; it no-ops once #851 lands. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N8YeCUhdg2tKdqyyY7z3sV
main has been red since #605 taught the name-keyed resolver to return npm-aliased copies, which broke two vex_consumed tests added by #738. Port #851's test update so this PR's CI goes green; it no-ops once #851 lands on main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uQyhodCtdJGrKaD7AAV1n
#605 taught the name-keyed npm resolver to probe bundled store trees, so it now finds aliased copies (node_modules/lp) and a nested host's store peers itself. Two vex_consumed tests from #738 assumed that set never held aliases, so main's CI went red after both merged. The tests now feed the alias-free set explicitly to keep covering alias expansion, and also check the resolver's own set reaches the same copies with no duplicates. No production code changes. Assisted-by: Claude Code:claude-opus-5-5 (cherry picked from commit 40dac07)
main is red: since the store-copy change (#605) the npm resolver already returns alias and nested-store copies, so two vex_consumed tests that assumed an alias-free set fail on main and on this branch. Same change as #851; it no-ops once main carries it. Refs #335 Assisted-by: Claude Code:claude-opus-5-5
#605 taught the name-keyed npm resolver to probe bundled store trees, so it now finds aliased copies (node_modules/lp) and a nested host's store peers itself. Two vex_consumed tests from #738 assumed that set never held aliases, so main's CI went red after both merged. The tests now feed the alias-free set explicitly to keep covering alias expansion, and also check the resolver's own set reaches the same copies with no duplicates. No production code changes. Assisted-by: Claude Code:claude-opus-5-5 (cherry picked from commit 40dac07)
* Start fix for #796 Assisted-by: Claude Code:claude-opus-5-5 * Find Bundler 4 standalone installs in ./bundle `bundle install --standalone` puts gems in ./bundle and the app loads them through bundle/bundler/setup.rb. Bundler 2 also recorded the path in .bundle/config, but Bundler 4 writes no config at all, so the gem crawler never looked in ./bundle. Agent apply then patched an ambient copy of the same gem (or said it was not installed), VEX attested not_affected while the app ran the unpatched copy, and the hosted stale-install warning stayed silent. Probe ./bundle as an install root when bundle/bundler/setup.rb is present, in the slot Bundler 2's recorded path used to take. Fixes #796. Assisted-by: Claude Code:claude-opus-5-5 * Document the standalone bundle install root List the Bundler 4 standalone ./bundle tree in the CLI contract's gem install-root order, so the documented roots match what the crawler probes. Assisted-by: Claude Code:claude-opus-5-5 * Port #851: fix vex alias tests after #605 main is red since #605: two commands::vex_consumed tests assumed the name-keyed resolver never returns npm-aliased copies, but #605 taught it to probe bundled store trees. This ports the tests-only fix from #851 so this PR's CI goes green; it no-ops once main carries #851. Assisted-by: Claude Code:claude-opus-5-5 --------- Co-authored-by: Claude <noreply@anthropic.com>
The merge in 46c9d4b took this branch's stale vex_consumed tests over feat/gradle-support's #605 updates: since #605 the name-keyed resolver already returns npm aliases and nested-store peers, so the old assertions that it returns none failed. Restore the gradle-side tests, keeping this branch's sbt hosted repo lookup in maven_copies. #664 (via main) refuses a linked `.socket/vendor` up front for every ecosystem with `vendor_dir_symlink_unsupported`, before the sbt shape check runs; expect that code for the linked vendor dir in the sbt vendor e2e, as the Maven CLI test already does. Co-Authored-By: Claude <noreply@anthropic.com>
…files (#590, #417) (#598) * Start fix for #590, #417 Assisted-by: Claude Code:claude-opus-5-5 * Refuse hosted runs from a workspace member Hosted scan and get read locks only in --cwd. Run from a pnpm workspace member (or a project whose lockfile-dir puts pnpm-lock.yaml elsewhere), they pinned nothing and still reported success, so pnpm kept installing the unpatched package (#590). Run from a cargo workspace member, they rewrote the member as a lockless project and broke every build of the workspace (#417). Both layouts are now refused before any takeover or write, exit 1, naming the directory to run from: redirect_pnpm_lockfile_elsewhere for pnpm, and the vendored cargo_manifest_not_workspace_root check, now shared, for cargo. Assisted-by: Claude Code:claude-opus-5-5 * Document the hosted workspace-member refusals Assisted-by: Claude Code:claude-opus-5-5 * Honor lockfileDir set by the workspace root A workspace root can move pnpm-lock.yaml with lockfileDir, and the key may be written quoted in pnpm-workspace.yaml. Hosted runs from a member of such a workspace, or of one with a quoted key, still reported success while pinning nothing. Both are now refused like any other member whose lock lives elsewhere. Assisted-by: Claude Code:claude-opus-5-5 * Honor pnpm workspace lockfile configuration precedence * Drop CHANGELOG entry from this PR Release notes are written when a release is cut, from the merged PR log and the code, so PRs no longer edit CHANGELOG.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Fix vex alias tests broken by store-copy merge #605 taught the name-keyed npm resolver to probe bundled store trees, so it now finds aliased copies (node_modules/lp) and a nested host's store peers itself. Two vex_consumed tests from #738 assumed that set never held aliases, so main's CI went red after both merged. The tests now feed the alias-free set explicitly to keep covering alias expansion, and also check the resolver's own set reaches the same copies with no duplicates. No production code changes. Assisted-by: Claude Code:claude-opus-5-5 * Read lockfileDir past a BOM and take the last A pnpm-workspace.yaml saved with a UTF-8 BOM, or one that sets lockfileDir twice, could hide a relocated lock from the member check, so a hosted run from a member still reported success while pinning nothing. The reader now skips the BOM and uses the last assignment. Assisted-by: Claude Code:claude-opus-5-5 --------- Co-authored-by: Claude <noreply@anthropic.com>
* Start fix for #652 Assisted-by: Claude Code:claude-opus-5-5 * Refuse gem lines pulled from git sources Hosted scans moved a gem declared with `gitlab:`, a custom `git_source(:name)` key or a string-keyed `"git" =>` option into the Socket source block. The option still overrode the block, so bundler kept loading the unpatched git checkout while scan reported the gem redirected and VEX attested it not_affected. String-keyed options such as `"require" => false` were also silently dropped. Both hosted and vendored modes now read gem options through one shared reader that understands every key spelling and treats any key outside bundler's non-source options as a source. Hosted mode also refuses a gem the lock resolves from a GIT, PATH or plugin section. Fixes #652 Assisted-by: Claude Code:claude-opus-5-5 * Add e2e and escape tests for gem git sources Adds a real-bundler capstone where the gem comes from a custom `git_source` key: the hosted scan must refuse it, write nothing and attest nothing, and bundler must still install the project. The option reader now also honors backslash escapes in single-quoted strings, so a quote inside a value cannot hide a later git option. Refs #652 Assisted-by: Claude Code:claude-opus-5-5 * Read the gem lock's git sections once per scan The new git/path refusal re-parsed Gemfile.lock for every patched gem, which made hosted bundler scans about 15% slower on the bench fixture (800 gems, 20 patched). The lock is now parsed once per rewrite; our own edits only touch GEM sections and CHECKSUMS, so the GIT/PATH membership read up front stays accurate. Refs #652 Assisted-by: Claude Code:claude-opus-5-5 * Fix vex alias tests broken by store-copy merge #605 taught the name-keyed npm resolver to probe bundled store trees, so it now finds aliased copies (node_modules/lp) and a nested host's store peers itself. Two vex_consumed tests from #738 assumed that set never held aliases, so main's CI went red after both merged. The tests now feed the alias-free set explicitly to keep covering alias expansion, and also check the resolver's own set reaches the same copies with no duplicates. No production code changes. Assisted-by: Claude Code:claude-opus-5-5 (cherry picked from commit 40dac07) * Retry PDM backtest cases on transport errors The PDM matrix runs against production PyPI and the public patch API. Over the last 7 days 25 pdm-compatibility runs failed on one random cell each, on unrelated PRs. The version, OS, shape, mode and check differed every time (rescanIdempotent, appliedExactlyOne, rescanAfterRelockApplies, ...). Each check judges a CLI scan, install or rollback. `Run` retries a command once, and only on a non-zero exit. The CLI usually reports an exhausted patch API fetch in its JSON while exiting zero, so the cell just fails a later check. Port backtest-poetry.py's case-level retry (#596). A case is re-run from a fresh directory, at most three attempts, only when every failed check recorded transport evidence from the operation it judged. Evidence is a failed command's request error, PyPI give-up, patch API 5xx or exhausted 429, or the same in the CLI's JSON error records. Functional failures are never retried, even when a later step raises a transport error. Failed attempts' logs go under attempts/ and are uploaded. A failing case now prints its failed checks' notes, since the job log alone never said why. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N8YeCUhdg2tKdqyyY7z3sV (cherry picked from commit 4329170) * Judge PDM rollback and VEX checks by their run Bugbot: the final hosted/vendored rollback checks, the unverifiable- write rollback, the refused-lock VEX and the reverted-lock VEX runs named no operation, so a transport failure there never made the case retryable. installedBytesPatched fails together with a blipped pdm sync and blocked the retry the same way. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N8YeCUhdg2tKdqyyY7z3sV (cherry picked from commit 6b0302a) --------- Co-authored-by: Claude <noreply@anthropic.com>
* Start fix for #736 Assisted-by: Claude Code:claude-opus-5-5 * Read only the gem lock Bundler actually loads A gems.rb project's gems.locked was invisible to the lock inventory, ledger recovery read only Gemfile.lock, and VEX discovery read both locks. A leftover redirected Gemfile.lock beside gems.rb + gems.locked therefore made vex attest not_affected while bundle install installed the unpatched gem from gems.locked. Add one resolver for the lock Bundler loads (honouring BUNDLE_GEMFILE and the app config) and route the inventory, gem_remotes, VEX discovery and the hosted engine through it. VEX still reads the ignored twin, but any Socket wiring there is diagnosed as unattributable instead of attested. Fixes #736 Assisted-by: Claude Code:claude-opus-5-5 * Test hosted engine on a gems.rb project Assisted-by: Claude Code:claude-opus-5-5 * Avoid a single-element loop in the polyglot test Assisted-by: Claude Code:claude-opus-5-5 * Note the gem lock reader fix in the changelog Assisted-by: Claude Code:claude-opus-5-5 * Drop CHANGELOG entry from this PR Release notes are written when a release is cut, from the merged PR log and the code, so PRs no longer edit CHANGELOG.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Port #851: fix vex alias tests broken by store-copy merge main is red since 4646693 (#605): two commands::vex_consumed tests assumed the name-keyed resolver never returns npm-aliased copies, which #605 changed. Same tests-only change as #851; it no-ops once main carries it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Ao6g9qAnawPfNxv11f3wM --------- Co-authored-by: Claude <noreply@anthropic.com>
* Start fix for #632 Assisted-by: Claude Code:claude-opus-5-5 * Pin yarn catalog deps in hosted mode A dependency declared "catalog:" in package.json was never patched by scan --mode hosted: the resolutions entry was keyed by the lock's expanded npm: range, but yarn matches resolutions before it expands the catalog. The scan reported success, then yarn install --immutable failed (YN0028) and a plain yarn install kept the unpatched release. Also route name@catalog: / name@catalog:<named> for every .yarnrc.yml catalog that maps the package to a pinned range. A re-run adds the selector to a pin written by an earlier release. Fixes #632 Assisted-by: Claude Code:claude-opus-5-5 * Test yarn catalog pins end to end Add a real-yarn check that a fresh checkout of a hosted-pinned catalog dependency installs the patched bytes under --immutable, an in-process scan + rollback round trip, and document catalog pins in the yarn berry hosted notes. Assisted-by: Claude Code:claude-opus-5-5 * Drop unrelated rustfmt churn cargo fmt --all also reformatted 127 files this fix doesn't touch (main isn't rustfmt-clean). Restore them and the untouched hunks of the edited files to main, so the PR only carries the catalog fix, its tests and the docs note. Assisted-by: Claude Code:claude-opus-5-5 * Keep unquoted yarn catalog ranges as their source text berry_catalog_selectors parsed .yarnrc.yml catalogs into serde_json Values, so an unquoted range like `1.10` became the number 1.1 and never matched the lock's `npm:1.10`: the `catalog:` selector was dropped while the pin was still confirmed. Yarn reads .yarnrc.yml with the failsafe schema, so deserialize the catalog tables as string tables instead, which keeps each scalar's source text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPHxnE5P1rkfCpHFFCwzFR * Port #851: fix vex alias tests broken by store-copy merge main fails commands::vex_consumed::tests::hosted_expands_alias_only_copies and hosted_reuses_expanded_npm_copies_and_merges_alias_variants since #605 landed alongside #738; #851 fixes the tests. Carry the same change so this PR's CI (coverage, test-release) is green; it no-ops once main has it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPHxnE5P1rkfCpHFFCwzFR --------- Co-authored-by: Claude <noreply@anthropic.com>
* Start fix for #756, #772 Assisted-by: Claude Code:claude-opus-5-5 * Report writes to pnpm/vlt store twin copies When a package has more than one pnpm or vlt peer-variant store copy, apply and rollback already patch (or restore) every copy, but they only reported what happened to the first one. A run that fixed only a twin copy said "already patched" (applied: 0), and a rollback that restored only a twin said "already original" (rolledBack: 0). Apply and rollback now share one store-copy fan-out and one fold, which merges each copy's per-file records into the result under the copy's on-disk path. The two private folds, which had drifted on which advisories they kept, are gone; both directions now carry only the ownership advisory from a copy. Fixes #756, #772. Assisted-by: Claude Code:claude-opus-5-5 * Test CLI reporting of store twin writes End-to-end regression for #756 through the real binary, on hand-built pnpm and vlt store layouts: apply that patches only a twin copy reports it as applied, and rollback that restores only a twin counts it as rolled back. Documents the copy-qualified file paths in CLI_CONTRACT. Assisted-by: Claude Code:claude-opus-5-5 * Keep --force skips in a twin copy local Under --force, a store twin missing a patched file skips it and still succeeds. The fold already dropped that copy's "all files skipped" note, but it carried the skipped file's NotFound record, so a package whose primary copy was already patched was reported as "applied" with no files instead of "already patched". Those records now stay with the copy, like its note. Assisted-by: Claude Code:claude-opus-5-5 * Fix vex alias tests broken by store-copy merge #605 taught the name-keyed npm resolver to probe bundled store trees, so it now finds aliased copies (node_modules/lp) and a nested host's store peers itself. Two vex_consumed tests from #738 assumed that set never held aliases, so main's CI went red after both merged. The tests now feed the alias-free set explicitly to keep covering alias expansion, and also check the resolver's own set reaches the same copies with no duplicates. No production code changes. Assisted-by: Claude Code:claude-opus-5-5 (cherry picked from commit 40dac07) --------- Co-authored-by: Claude <noreply@anthropic.com>
* Start fix for #798 Assisted-by: Claude Code:claude-opus-5-5 * Refuse npm VEX when the twin lock lacks the pkg With both npm-shrinkwrap.json and package-lock.json committed, lockfile-only `vex` attested a patch that only one lock wired when the other lock had no entry for the package at all. npm 12 installs from package-lock.json and re-resolves a missing entry from the registry, so the checkout installed unpatched bytes while the VEX document said `not_affected`. A twin lock with no entry for the package now contests the wiring the same way a registry entry does (`patched_ref_unattributable`), in both directions and for hosted and vendored wiring. A twin that holds the package only at another version still contests nothing: npm installs that version, not unpatched bytes of the patched one. Fixes #798 Assisted-by: Claude Code:claude-opus-5-5 * Make the dual-lock read test use agreeing twins The test that proves both npm locks are read wired each package in only one lock. After #798 such a pair is contested (npm re-resolves the package missing from the other lock), so the fixture now has each lock wire both packages. It still proves both locks are read (4 refs) and that the v2 legacy mirror adds nothing. Refs #798 Assisted-by: Claude Code:claude-opus-5-5 * Keep patch refs out of a vex test's messages CodeQL flagged the new dual-lock test for printing the wired patch reference (which holds the patch uuid) in an assertion message. The message now names the wiring mode instead. Refs #798 Assisted-by: Claude Code:claude-opus-5-5 * Fix vex alias tests broken by store-copy merge #605 taught the name-keyed npm resolver to probe bundled store trees, so it now finds aliased copies (node_modules/lp) and a nested host's store peers itself. Two vex_consumed tests from #738 assumed that set never held aliases, so main's CI went red after both merged. The tests now feed the alias-free set explicitly to keep covering alias expansion, and also check the resolver's own set reaches the same copies with no duplicates. No production code changes. Assisted-by: Claude Code:claude-opus-5-5 (cherry picked from commit 40dac07) * Contest a twin npm lock that lacks the wired version A twin lock that held the package only at another version still let the wired ref through. npm keeps that entry only while it satisfies package.json, and otherwise fetches the wired version unpatched from the registry, so the lock alone can't vouch for it. The twin now contests unless it has an entry for the same name@version at any path. Lock pairs the rewriters keep in sync share that set, so they are unaffected. Refs #798 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TtZrsd52E6vxhvF9hpLVSw --------- Co-authored-by: Claude <noreply@anthropic.com>
* Start fix for #627 Assisted-by: Claude Code:claude-opus-5-5 * Refuse vendoring over a symlinked lockfile Vendored mode renamed its rewritten lockfile (or package.json, pnpm-workspace.yaml, nuget.config) over a symbolic link, turning a shared lock into a detached copy: the link's target, the lock other checkouts install from, stayed unpatched, and revert never restored the link. Hosted mode already refused this. The vendored group commit now refuses before writing anything when a file it would change is a symlink or sits under a symlinked directory. The run exits 1 with the same redirect_symlinked_file_unsupported error hosted mode uses, and leaves the link, its target and the vendor ledger untouched. A --dry-run flags each symlinked wiring file with a vendor_would_refuse_symlinked_file advisory. Fixes #627 Assisted-by: Claude Code:claude-opus-5-5 * Gate only symlinked files, as hosted does Writing into a symlinked directory goes through the link rather than replacing it, so refusing it would newly break projects that link a whole directory. Check the changed file itself, which matches the hosted guard. Also add a real-yarn e2e for a symlinked yarn.lock. Assisted-by: Claude Code:claude-opus-5-5 * Format the symlinked yarn.lock e2e Assisted-by: Claude Code:claude-opus-5-5 * Warn about symlinked files in scan/get dry runs scan and get --mode vendored --dry-run stop at the ledger preview and never reach the vendor loop, so they gave no hint that the real run would refuse a symlinked lockfile. The preview's would_vendor and would_revendor rows now carry the same symlink warning that vendor --dry-run emits, and human output prints it. Assisted-by: Claude Code:claude-opus-5-5 * Narrow dry-run symlink warnings to real writes The vendored dry run warned about symlinked files a vendored run only reads (.yarnrc.yml, vlt.json, node_modules/.modules.yaml), which the real run never writes and so never refuses. It also warned for packages already in sync, whose re-run writes nothing. Both produced false predictions of the symlink refusal. The warning now covers only files a vendored run can rewrite, and skips packages the dry run previews as already vendored. Assisted-by: Claude Code:claude-opus-5-5 * Warn about symlinked pom.xml and hatch.toml too The narrowed dry-run warning dropped files vendored Maven and Hatch really rewrite: the root pom.xml, .mvn/maven.config and hatch.toml. A symlinked root pom.xml was still refused by the real run with no dry-run hint. Add them to the list of vendored write targets. Assisted-by: Claude Code:claude-opus-5-5 * Port #851 fix for vex alias tests broken on main main has been red since #605: two vex_consumed tests still assumed the name-keyed resolver was alias-blind, so the CLI lib tests fail on every branch built on main. This carries the same test-only change as #851 and becomes a no-op once #851 lands. Assisted-by: Claude Code:claude-opus-5-5 --------- Co-authored-by: Claude <noreply@anthropic.com>
* Start fix for #804 Assisted-by: Claude Code:claude-opus-5-5 * Fix rollback of pip-written pylock.toml `pip lock` writes PEP 751's array-of-tables spelling (`[[packages.wheels]]` with a `[packages.wheels.hashes]` sub-table), but the hosted upstream restore only read inline `wheels = [{ ... }]` arrays. Every pip sibling looked artifact-free, so `rollback`, `remove` and the hosted -> vendored takeover always refused a pip lock with "no sibling registry package shows ...", leaving users with a hosted patch they could not undo. The restore now reads artifacts in either spelling, writes the entry back in the siblings' spelling, and, since pip records only the one artifact it selected, restores only the release's wheel (or its sdist when it has no wheel), refusing a release with several wheels. The refusal no longer blames "this uv release" for a pip-written lock. Fixes #804 Assisted-by: Claude Code:claude-opus-5-5 * Port vex alias test fix from #851 main has been red since #605 taught the npm copy resolver to probe bundled store trees: two vex_consumed alias tests (#738) still assumed the resolver never returns npm-aliased copies, so the CLI lib tests fail on every PR's merge ref. This ports #851's tests-only fix so the PR's CI reflects its own change; it no-ops once #851 lands on main. Assisted-by: Claude Code:claude-opus-5-5 --------- Co-authored-by: Claude <noreply@anthropic.com>
* Start fix for #372 Assisted-by: Claude Code:claude-opus-5-5 * Accept vlt 1.3 brotli lock nodes vlt 1.3 marks a lock node that fetches the registry's Brotli (.tar.br) tarball with a new flag bit, 4, in slot [0]. socket-patch only accepted flags 0-3, so hosted mode refused such a lock as "not canonical" and exited 0 with nothing redirected, and vendored mode failed with a misleading lockfile-version error. Accept flags 0-7. When a pin or vendored wiring points a node at a .tgz or local directory, clear the brotli bit as vlt would save it; reverts put the recorded bit back with the original slots. The vlt heal now reinstalls brotli prod and dev nodes like any other. Fixes #372 Assisted-by: Claude Code:claude-opus-5-5 * Port #851: fix vex alias tests broken by store-copy merge main is red since 4646693 (#605): two commands::vex_consumed tests assumed the name-keyed resolver never returns npm-aliased copies. Same test-only change as #851; it no-ops once main carries it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NhRxWtzEYpLyrByBegiiRy * Drop unrelated cargo fmt --all churn from the vlt brotli fix The fix commit b3996a6 also reformatted 123 files it does not otherwise touch (the output of cargo fmt --all on a tree main has not formatted). Every one of those files is byte-identical to rustfmt run over main's version, so this restores them to main. The PR now only touches the vlt lock, redirect and heal code plus the ported #851 test fix, which keeps the review small and stops the churn from conflicting with every other open PR. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
* Start fix for #432 Assisted-by: Claude Code:claude-opus-5-5 * Pin npm aliases in the npm 6 lock mirror A lockfileVersion 2 package-lock.json keeps a legacy `dependencies` mirror for npm 6, which spells an alias install as `"lp": {"version": "npm:left-pad@1.3.0"}`. Hosted scans matched mirror nodes on a plain version only, so the alias node silently stayed on the registry, and a lockfileVersion 1 alias lock pinned nothing at all ("no package-lock.json entry"). Rollback could not restore such a node either. Every reader of the legacy tree now decodes the alias through one helper. Hosted scans rewire the alias node with the rest, and rollback restores it. npm 6 fetches an aliased dependency from the registry whatever `resolved` says, so under npm 6 the pinned lock fails closed (EINTEGRITY) instead of installing unpatched bytes, and the run warns `redirect_npm_legacy_alias_client`. Refs #432 Assisted-by: Claude Code:claude-opus-5-5 * Vendor npm aliases in the npm 6 lock mirror Vendoring skipped the v2 mirror node of an npm alias with `vendor_legacy_alias_skipped`, so npm 6 installed the unpatched registry tarball through it. npm 6 does install an alias node from a `file:` resolved (checked against npm 6.14.18), so the node is now rewired like every other mirror node and revert restores it. Refs #432 Assisted-by: Claude Code:claude-opus-5-5 * Withhold VEX when the npm 6 mirror is unpatched Lockfile-only VEX read a v2 lock's `packages` half only, so it attested `not_affected` for a package whose legacy mirror (what npm 6 installs from) still resolved to the registry, as locks written before this fix do for npm aliases. Such a ref is now diagnosed as unattributable and not attested. A lockfileVersion 1 alias node is read as an install of its target package. Fixes #432 Assisted-by: Claude Code:claude-opus-5-5 * Let a stale npm 6 mirror contest the sibling lock When npm-shrinkwrap.json's legacy mirror still resolved a package from the registry, VEX dropped the shrinkwrap's own ref but still attested the same package from package-lock.json, although npm 6 installs from the shrinkwrap. A mirror node off Socket now counts as resolving the package elsewhere, so the sibling lock's ref is contested too. Refs #432 Assisted-by: Claude Code:claude-opus-5-5 * Port vex alias test fix from #851 Main has been red since #605: two commands::vex_consumed tests assume the name-keyed resolver never returns npm-aliased copies, but #605 taught it to probe bundled store trees. Port #851's test-only fix so this PR's CI runs on a green base. It becomes a no-op once #851 lands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EgBZwmqgXLZaRGyfDFoWwp --------- Co-authored-by: Claude <noreply@anthropic.com>
* Retry PDM backtest cases on transport errors The PDM matrix runs against production PyPI and the public patch API. Over the last 7 days 25 pdm-compatibility runs failed on one random cell each, on unrelated PRs. The version, OS, shape, mode and check differed every time (rescanIdempotent, appliedExactlyOne, rescanAfterRelockApplies, ...). Each check judges a CLI scan, install or rollback. `Run` retries a command once, and only on a non-zero exit. The CLI usually reports an exhausted patch API fetch in its JSON while exiting zero, so the cell just fails a later check. Port backtest-poetry.py's case-level retry (#596). A case is re-run from a fresh directory, at most three attempts, only when every failed check recorded transport evidence from the operation it judged. Evidence is a failed command's request error, PyPI give-up, patch API 5xx or exhausted 429, or the same in the CLI's JSON error records. Functional failures are never retried, even when a later step raises a transport error. Failed attempts' logs go under attempts/ and are uploaded. A failing case now prints its failed checks' notes, since the job log alone never said why. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N8YeCUhdg2tKdqyyY7z3sV * Judge PDM rollback and VEX checks by their run Bugbot: the final hosted/vendored rollback checks, the unverifiable- write rollback, the refused-lock VEX and the reverted-lock VEX runs named no operation, so a transport failure there never made the case retryable. installedBytesPatched fails together with a blipped pdm sync and blocked the retry the same way. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N8YeCUhdg2tKdqyyY7z3sV * Port #851: fix vex alias tests broken on main Main is red since #605 (4646693): two commands::vex_consumed tests assume the name-keyed resolver never returns npm-aliased copies, and #605 taught it to find them. This fails socket-patch-cli --lib in coverage and test on every PR. Port #851's test-only fix so this PR can go green; it no-ops once #851 lands. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N8YeCUhdg2tKdqyyY7z3sV --------- Co-authored-by: Claude <noreply@anthropic.com>
* Start fix for #709 Assisted-by: Claude Code:claude-opus-5-5 * Verify gems in out-of-tree bundle config paths A .bundle/config "path" outside the project (bundle config set --local path /opt/bundle) is refused as an install root because apply writes there. That refusal also hid the root from the read-only checks, so hosted scan gave no stale-install warning and both the in-run --vex and a later `vex` attested not_affected while bundler kept loading the unpatched gem from that path. The refused root is now exposed as a verification-only store: the hosted stale-install probe and vex's installed-copy lookup read it, while apply and rollback still never write there. A stale copy there gets the project-local remedy. Fixes #709 Assisted-by: Claude Code:claude-opus-5-5 * Test hosted scan over an out-of-tree bundle path Covers #709 end to end: a stale gem under a .bundle/config path outside the project now warns with the project-local remedy, and the same run's --vex does not attest it. Assisted-by: Claude Code:claude-opus-5-5 * Run the out-of-tree bundle path probe test The regression test for #709 was nested inside another test function, so it compiled but never ran. Move it back to module level. Assisted-by: Claude Code:claude-opus-5-5 * Honor BUNDLE_IGNORE_CONFIG for the bundle path With BUNDLE_IGNORE_CONFIG set, bundler reads no config file, so a .bundle/config path is neither an install root nor a root bundler loads from. Discovery now skips the app config's BUNDLE_PATH in that case, the same way the cache-path and Gemfile readers already do, so leftover gems under that path no longer raise a stale-install warning or fail VEX. Assisted-by: Claude Code:claude-opus-5-5 * Port #851: fix vex alias tests broken on main Main is red since #605: two vex_consumed tests assumed the copy resolver never returns npm-aliased copies, which #605 changed. This ports the test-only fix from #851 so this PR's CI can go green; it becomes a no-op once #851 lands. Assisted-by: Claude Code:claude-opus-5-5 --------- Co-authored-by: Claude <noreply@anthropic.com>
Final-head CI is complete: 452 successful checks, 6 skipped; no failures or pending checks. Bugbot passed, there are no unresolved review threads, and the PR is mergeable.
Fixes #601 and #603. In pnpm, vlt, Bun and Deno layouts, agent
apply,rollbackandvexmust discover every relevant physical copy. Previously, finding a normal installation could hide a copy bundled inside another store entry; agent VEX also omitted peer variants that apply already patched. VEX could therefore attest a package while a consumed copy remained unpatched.The resolver now visits the owning package's bundled
node_moduleseven when its store entry is skipped by the pending-name filter. Agent VEX uses the shared npm store-variant expansion so an unpatched bundled or peer copy prevents attestation.CLI_CONTRACT.mdandCHANGELOG.mddescribe the every-copy behavior.Review corrections preserve that coverage while bounding traversal and repeated work:
node_modulesdirectories separately for importer and store-entry mode. Cyclic bundled links terminate; root-first path choices, legitimate linked copies, and the two different link policies are retained.Validation on
b92456b3:unused_variablesallowed for the existing macOS Python-crawler warning. The final committed files match the tested hashes; independent source/evidence review found no remaining issue, and the commit merges cleanly with main045d7ec7.b92456b3. A Windows Bun 1.2.23 patch-detail API timeout was retried once at the failed-job level; all 49 cases passed on the retry, including the expected workspace refusal.#599, concerning orphaned Bun store entries, remains a separate reachability change.
Note
Medium Risk
Changes npm install discovery and VEX copy sets for security-sensitive apply/verify paths; bounded traversal and deduplication reduce performance and infinite-loop risk but behavior shifts when multiple physical copies exist.
Overview
Fixes #601 and #603 so agent
apply,rollback, andvextreat every physical npm copy that can actually run—not only the “normal” importer-linked install.The npm resolver now walks bundled
node_modulesinside skipped pnpm/vlt/Bun/Deno store entries when the samename@versionwas already found elsewhere, with cycle-safe traversal so bundled trees that link back to ancestors do not hang or drop legitimate copies.with_store_peer_variant_copiescentralizes peer/index/alias store expansion (with one scan per store identity per batch). Manifest copy lookup applies it for npm; hostedvexreuses already-expanded installed paths and only expands new alias paths, avoiding redundant store rescans.Docs (
CHANGELOG,CLI_CONTRACT) state that agent verification must hash every crawler copy, including peer variants and bundled store copies. Regression tests cover apply/rollback on bundled layouts, agent VEX omission when any store copy is pristine, and hosted-merge behavior.Reviewed by Cursor Bugbot for commit b92456b. Configure here.