Repository navigation
Fix npm rewiring git/URL/file lock entries (#326) - #345
Mikola Lysenko (mikolalysenko) merged 8 commits into
Conversation
Assisted-by: Claude Code:claude-opus-5-5
npm installs a git, remote-tarball or file: dependency from the dependent's spec and ignores the lock entry's resolved. Hosted and vendored mode rewired those entries anyway, so the scan reported the package patched and vex attested it while npm ci installed the original bytes. Both rewriters now skip such entries with a loud stays-UNPATCHED warning (vendored refuses when no registry copy is left), and vex no longer attests a name@version while a non-registry copy of it is in the lock. Fixes #326 Assisted-by: Claude Code:claude-opus-5-5
4a797e2 to
ad605fe
Compare
Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
npm 12 refuses remote-tarball specs unless allow-remote permits them, so the new vendored e2e now passes --allow-remote=all there. The VEX test messages and the new diagnostic no longer print patch URLs or uuids. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
|
[agent]
This PR only changes npm lock handling and npm VEX discovery; no PyPI or Poetry code is touched. I've re-run the failed jobs of run 36755620097 once. If it fails again, I'll treat it as a real failure and dig in. Generated by Claude Code |
|
[agent]
This PR doesn't change any vlt code. I can't re-run the job yet because workflow run 36755620093 still has jobs in progress (the API returns 403). I'll re-run it once when the run finishes, and treat a second failure as real. Generated by Claude Code |
The hosted rewriter and the vendored backend skipped a git, URL or file: packages entry, but still rewired its lockfileVersion 2 legacy dependencies mirror when that mirror stored the plain version. Each legacy node now maps to the packages key it mirrors, and a node whose twin is non-registry is left alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LycgFGuki2BqZ25VNhwxJ5
|
[agent]
This PR only changes npm lock handling and npm VEX; no Composer code is touched. I can't re-run it yet because run 36758922826 still has jobs in progress (403). I'll re-run it once when the run finishes, and treat a second failure as real. Generated by Claude Code |
|
BugBot review Generated by Claude Code |
Bring #326's fix onto the v5 workflow from #277 so the PR merges cleanly. Main dropped the pre-index npm/yarn oracles from the lock index equivalence tests, so the PR's oracle tweak goes with them; the vex npm discover imports follow main's move to formats::pnpm and keep the npm_origin import; the changelog entry moves into main's condensed Unreleased "Fixed" section. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQoii5oP1pwcJh5mzzo1HU
|
bugbot run Generated by Claude Code |
|
[burn-down agent] Ready for review on Generated by Claude Code |
Union the CHANGELOG Fixed entries from both sides. Co-Authored-By: Claude <noreply@anthropic.com>
|
bugbot run Generated by Claude Code |
Co-Authored-By: Claude <noreply@anthropic.com>
|
bugbot run 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 f1e0108. Configure here.
|
[agent] Ready for review on
Slack announcement not sent this run: the Slack send tool isn't available in the agent session. The next run will retry. Generated by Claude Code |
Keeps both CHANGELOG entries (gem manifest fix and the npm non-registry entry fix from #345). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L6i4cQ51yarBFnFs2b8HRx
Bun installs a dependency declared by remote tarball URL or `file:` tarball from that spec, never from the registry. When the lock also holds a registry copy of the same name@version (for example nested under a dependent), hosted and vendored mode rewired only the registry copy and reported plain success, while the root copy that the app loads stayed unpatched after `bun install --frozen-lockfile`. Lockfile VEX then attested not_affected for that name@version. That covers vendored (default and --no-verify), hosted --no-verify, and hosted in a lockfile-only checkout. This is the Bun twin of npm's #326 / #345. The tarball tuple / record carries no version of its own, so its version is read from the artifact leaf, `<name>-<semver>.tgz` (`user_tarball_version`, shared by every Bun path). Then: - the hosted bun.lock and bun.lockb rewriters leave such a copy alone with a `redirect_bun_non_registry_entry_skipped` stays-UNPATCHED warning, and keep the uuid out of the in-run VEX assumptions; - the vendored text and binary backends warn with `vendor_non_registry_entry_skipped`, and refuse with `vendor_lock_entry_not_rewritable` when no registry copy is left; - lockfile VEX discovery counts the copy as an unpatched install of that version, so every ref for it in the lock is withdrawn with a patched_ref_unattributable diagnostic and other locks are contested. Regression tests use fixture text locks and real Bun 1.1.45 bun.lockb fixtures (URL and file: shapes) generated for this issue. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The npm/hosted bench drifted +15% wall / +12% CPU over a week of main (2463257..9c43dfc). The new npm lock passes from #345, #491, #646 and #799 each added a walk of the 3000-entry package-lock.json, on top of two costs that scale badly with every extra walk: - Every hosted scan ran a full second lockfile discovery after the rewrite (`hosted_state_from_lockfiles`) only to classify a hosted vs vendored takeover, even with no vendored ledger to overlap, and then a THIRD one inside `classify_overlap_takeover_with` when there was one. The classifier now discovers once, and not at all when the vendored ledger has no entries (the hosted common case). - `Discovery::resolved_elsewhere` deduped with `Vec::contains` on every call, so recording a lock's registry entries was quadratic in its size. `finalize` already sorts and dedups the list and every earlier reader only asks whether some entry matches, so the per-call check is gone. Also trimmed per-entry allocations on the new passes: the npm extractor builds each entry's purl once (it was built for `mention` and again for `unwired`), `resolved_is_non_registry` no longer formats a prefix per entry, and `npm_spec_is_registry` no longer lowercases every edge spec. Instructions retired for `scan --json --dry-run` on the bench's npm fixture (median of 9, macOS): base 2463257 2.647G, branch head 2.755G (+4.1%), this commit 2.353G (-11.1% vs base, -14.6% vs head). Scan output is byte-identical to before. Regression test: `takeover_classifier_discovers_at_most_once` counts discoveries (test-only counter in `discover_wiring`); new unit tests pin the rewritten registry/tarball predicates. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LLM Description written by Claude Code:claude-opus-5-5
Fixes #326
Summary
scan --mode hostedandvendorno longer rewire an npm lock entry that npm installs from a git, remote-tarball (URL) orfile:spec, andvexno longer attests one. Before this change both modes rewrote that entry'sresolved/integrityand reported the package patched, andvexattestednot_affected. Meanwhilenpm cifetched the git checkout or URL again and installed the original bytes.Changes:
vendor/npm_origin.rs(new):npm_non_registry_entries(lock)returns thepackageskeys npm installs from a non-registry source, each with a reason. An entry counts as non-registry in two cases:npm_spec_is_registrymirrors npm-package-arg: versions, ranges, dist-tags andnpm:aliases count as registry; git, GitHub shorthand, URLs, paths and tarball names don't. The edges come from every entry'sdependencies/optionalDependencies/devDependencies/peerDependencies, including the root and workspace members, resolved in node's lookup order.resolvedisgit+…/github:…/… or afile:outside.socket/vendor/.The spec check is what catches a URL spec whose
resolvedlooks exactly like a registry tarball. It also catches an entry that has already been rewired: our own wiring only rewritesresolved, never the dependent's spec.Hosted (
patch/redirect/mod.rs): such an entry is skipped withredirect_npm_non_registry_entry_skipped, a stays-UNPATCHED warning that names the entry and the spec. It still counts as a match, soredirect_npm_entry_not_foundstays quiet.Vendored (
vendor/npm_lock.rs): such an entry is skipped withvendor_non_registry_entry_skipped. When no rewritable copy is left, the vendor refuses withvendor_lock_entry_not_rewritable(the existing bundled/link refusal, with its wording widened) and writes nothing.v2 legacy mirror (both modes): in a lockfileVersion 2 lock, each legacy
dependenciesnode maps to thepackageskey it mirrors (legacy_packages_key). A node whose twin is non-registry is left alone, even when it stores the plain version.VEX (
vex/discover/npm.rs): each lock drops every ref whosename@versionhas a non-registry copy in that lock, diagnosed aspatched_ref_unattributable. That copy also counts as resolved elsewhere, so it contests the same version in the sibling npm lock and in other lock types. This covers locks that were rewired before this fix, and a registry copy that is wired while a git copy of the same version stays unpatched next to it.Docs: a CHANGELOG
Fixedentry, and the CLI_CONTRACT npm row.Root cause
The npm lock rewriters pick entries by name and version only:
rewrite_one_npm_lockandrewrite_npm_v2_depsfor hosted,scan_lock_matchesandrewrite_legacy_treefor vendored. Onlylink/inBundleentries are excluded. npm installs a git, URL orfile:dependency from the dependent's spec and ignores the lock'sresolved, so the rewrite had no effect at install time. VEX discovery trusted the rewiredresolvedin the same way.Not affected: lockfileVersion 1. There, a git/URL/
file:entry'sversionis the spec itself, so it never matches a patch'sname@version.Test evidence
mainfile:tarball (direct)patch::redirect::tests::npm_non_registry_entries_are_skipped_with_loud_warningnpm_nested_git_copy_is_skipped_and_registry_copy_rewirednpm_v2_legacy_mirror_of_a_non_registry_entry_is_not_rewiredfile:(refuses, writes nothing)vendor::npm_lock::tests::non_registry_only_instances_refuse_and_write_nothingnested_git_instance_is_skipped_with_warningv2_legacy_mirror_of_a_git_instance_is_not_rewrittenfile:spec, nested git copy contests the wired copy, plus a registry controlvex::discover::npm::tests::entries_npm_installs_from_a_non_registry_spec_are_not_attestedvendorthenvexe2e_vendor_npm_build::npm_vendor_refuses_a_remote_tarball_dependencyapplied: 1)vendor::npm_origin::tests::*(9 tests: spec classification, node lookup, workspaces, aliases)Red was shown by stashing the three core source files and re-running the new tests against
main's implementation. For the two v2-mirror tests, it was shown by disabling the new guard.cargo test -p socket-patch-core --all-features --lib -- npm vex::discover redirect: 1494 passed and 1 failed. The failure isvlt_heal::tests::an_unremovable_hidden_lock_keeps_every_store_entry, which fails on the unchanged branch too (the sandbox runs as root).SOCKET_PATCH_NPM_E2E_REQUIRED=1 cargo test -p socket-patch-cli --all-features --test e2e_vendor_npm_build --test e2e_redirect_npm_build -- --include-ignored(real npm 10.9.7, Node 22): 14 + 15 passed. The new e2e also passes against npm 12.1.0.EALLOWREMOTE). That made the new e2e fail its fixture install in CI'sinstall-proof (12.x)legs, so it now passes--allow-remote=allon npm ≥ 12.cargo clippy --workspace --all-features -- -D warnings: clean.cargo fmt --all -- --check: the new code is rustfmt-clean. The remaining diffs are already onmain, in files and hunks this PR doesn't touch.cargo test --workspace --all-features --no-fail-fast(on d53ef13): 10,407 passed and 22 failed. The 22 are the same sandbox-only set reported on Fix #258: state the real Maven Trusted Checksums floor #322, Fix Poetry venv discovery to match Poetry (#327, #329) #330 and Fix npm VEX attesting packages with an unpatched bundled copy (#325) #337: write-failure and permission tests that fail because the sandbox runs as root, plus the peak-RSSstage_local_artifact_caps_oversized_artifact_before_buffering. None of them are in npm or VEX code.install-proof(npm registry) and macOS Poetry 1.8.5 (PyPI).scan --vexgoes through the same discovery gate.Notes
vex/discover/npm.rsnext to Fix npm VEX attesting packages with an unpatched bundled copy (#325) #337, which adds a same-shaped gate for bundled copies. The two edit different functions (extract_package_lockhere,push_uncontestedthere), so whichever lands second should need at most a trivial merge.overridesentry forces a URL spec onto a registry-range edge, the lock alone doesn't show it. Theresolvedcheck still catches forced git andfile:sources.🤖 Generated with Claude Code
https://claude.ai/code/session_01LycgFGuki2BqZ25VNhwxJ5
Note
Medium Risk
Changes npm lock rewrite, vendoring, and VEX attestation gates for a common dependency pattern; incorrect classification could skip legitimate registry patches or still over-attest, but behavior is covered by extensive tests and fail-closed warnings.
Overview
Fixes false “patched” reporting for npm deps installed from git, URLs, or
file:specs (#326). npm honors the dependent’s spec, not the lock entry’sresolved, so rewiring those entries did not change whatnpm ciinstalled while hosted/vendored flows and VEX still claimed success.A shared
npm_non_registry_entriesclassifier (newvendor/npm_origin.rs) drives consistent behavior: hosted scan skips withredirect_npm_non_registry_entry_skipped, vendor skips or refuses withvendor_non_registry_entry_skipped/vendor_lock_entry_not_rewritable, and VEX discovery drops attestations for affectedname@versionpairs (patched_ref_unattributable). Lockfile v2 legacydependenciesmirrors of skipped entries are left untouched. Docs and e2e/unit tests cover git, URL tarball,file:, and mixed registry/non-registry trees.Reviewed by Cursor Bugbot for commit f1e0108. Configure here.
Generated by Claude Code