Skip to content

Fix npm overrides of git deps being refused (#490) - #491

Open
Mikola Lysenko (mikolalysenko) wants to merge 6 commits into
mainfrom
agent/fix-npm-origin-overrides
Open

Mikola Lysenko (mikolalysenko) wants to merge 6 commits into
mainfrom
agent/fix-npm-origin-overrides

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

LLM Description written by Claude Code:claude-opus-5-5

Fixes #490

Summary

A dependency can declare a package from git, a URL or file:. When the project's overrides pin that package back to a registry version, npm installs the registry release. Since #345, socket-patch still treated that copy as coming from git:

  • hosted scan skipped it with redirect_npm_non_registry_entry_skipped, exited 0 and left the package unpatched;
  • vendored scan failed with vendor_lock_entry_not_rewritable;
  • vex refused to attest an install that was correctly patched.

All three now treat the overridden copy as the registry install it is. They patch it and attest it.

Root cause

vendor::npm_origin::npm_non_registry_entries decides which lock entries come from the registry. The hosted rewriter, the vendored backend and VEX discovery all share it. It flagged an entry whenever some dependent's raw spec for that name wasn't a registry spec, and it never looked at overrides. npm doesn't record overrides in the lock (checked with npm 10: the root "" entry has no overrides, while resolved is the registry tarball and pkga's entry keeps github:…), so the lock alone can't tell this case apart.

Changes

  • core vendor/npm_origin.rs: new NpmOverrides, parsed from the root package.json (overrides plus the root dependency specs, so $name references resolve). npm_non_registry_entries(lock, &overrides) drops a non-registry edge when an override that clearly applies to it gives a registry spec. That covers:

    • top-level rules;
    • rules nested under the dependent or one of its physical ancestors, outermost first, with an optional exact-version parent selector;
    • "." values;
    • $name references;
    • a target selector equal to the edge's own spec.

    As in npm, the rule scoped to the closest ancestor wins, then the most deeply nested one. Two equally close rules that disagree count as unclear. Anything else unclear (range selectors, rules scoped to unrelated packages, overrides to another git/file: source, missing $ references) keeps the npm hosted and vendored modes rewire git-sourced lock entries, so npm ci silently installs the unpatched git bytes while scan and VEX report success #326 behavior, so a doubtful copy stays reported as UNPATCHED and is never attested.

  • Hosted: the engine reads package.json as advisory input when an npm candidate meets an npm lock. A symlinked or unreadable in-memory entry is left out, never refused. The in-memory selector fetches it (EXTRA_TEXT_FILES). The rewriter never writes it.

  • Vendored (vendor/npm_lock.rs): vendor_npm and the download-plan preflight read it once and pass it to the scan, the sibling-lock scan and the rewire.

  • VEX (vex/discover/npm.rs): lockfile discovery reads it quietly (no diagnostic, no recognition) next to the lock.

  • CHANGELOG.md: added to the existing npm hosted and vendored modes rewire git-sourced lock entries, so npm ci silently installs the unpatched git bytes while scan and VEX report success #326 entry.

The npm/pypi/gem wrappers only dispatch to the binary, so they need no change.

Tests

Issue path Regression test Without fix With fix
classification vendor::npm_origin::tests::issue_490_* (5 tests: registry overrides of every supported shape, fresh and already-redirected locks; innermost wins; nested/scoped ancestors) 4 FAILED ok
classification (control) issue_490_overrides_that_do_not_clearly_apply_keep_the_edge ok ok
closest ancestor wins (Bugbot finding) issue_490_the_closest_ancestor_rule_wins_whatever_the_key_order FAILED (old depth-only tie-break) ok
equally close rules disagree (guard) issue_490_equally_close_rules_that_disagree_keep_the_edge ok ok
hosted rewriter patch::redirect::tests::issue_490_a_git_edge_overridden_to_the_registry_is_redirected FAILED ok
hosted engine read (memory + disk; symlinked/unreadable manifest not refused) hosted::engine::tests::issue_490_the_root_manifest_overrides_reach_the_npm_rewriter n/a (new read) ok
vendored vendor::npm_lock::tests::issue_490_a_git_edge_overridden_to_the_registry_is_vendored FAILED (vendor_lock_entry_not_rewritable) ok
vex vex::discover::npm::tests::issue_490_a_git_edge_overridden_to_the_registry_is_attested (hosted + vendored wiring) FAILED (0 refs) ok
real npm e2e e2e_redirect_npm_build::npm_redirect_overridden_git_dependency_installs_patched_bytes — see below

"Without fix" was produced by short-circuiting NpmOverrides::replacement to None (and, for the closest-ancestor test, by restoring the old depth-only ranking). The existing #326 tests (npm_non_registry_entries_are_skipped_with_loud_warning, non_registry_only_instances_refuse_and_write_nothing, entries_npm_installs_from_a_non_registry_spec_are_not_attested, the golden indexed_npm_lock_rewrite_matches_golden) still pass unchanged.

Real-npm e2e. The new test reuses the hosted capstone fixture with a real npm install of the #490 project shape. Each run checks four things:

  • scan --mode hosted --vex reports redirected: 1, writes the lock pin and emits one in-run VEX statement;
  • a fresh npm ci installs the patched bytes byte for byte;
  • the hash-verified vex attests them;
  • rollback restores the lock.

CI's npm compatibility matrix runs it end to end on npm 9.9.4, 10.9.9, 11.20.0, 12.0.0 and 12.1.0. It is N/A, not a skip (skip() fails the pinned REQUIRED legs), in two cases:

  • npm < 8.3, which has no overrides;
  • npm 9.0.0, where npm itself can't install the project and dies with Invalid comparator: github:… when an override covers a git spec.

Locally the rollback step can't reach registry.npmjs.org through this sandbox's TLS proxy. The unchanged main capstone hits the same limit, so CI is the proof for that step.

CI on c657128. All 11 workflows pass: CI, npm, pnpm, Bun, vlt, Poetry, PDM, Pipenv, Go, Composer and the GHA audit. Bugbot's latest review of this commit found no new issues, and its one earlier finding is fixed and resolved.

Local runs:

  • cargo clippy --workspace --all-features -- -D warnings: clean.
  • cargo test --workspace --all-features --no-fail-fast: everything passes except:
    • 12 chmod/read-only "write failure" tests, which can't fail under uid 0 in this sandbox. This is the same set as on other branches (see Fix berry mode takeover reverting before gates (#468, #369) #470).
    • golang_get_uuid_hosted_day2_machine_builds, which hit a full disk mid-run (link: no space left on device). It passes on re-run.
  • cargo fmt: the files this PR touches are rustfmt-clean apart from lines that were already unformatted on main. CI doesn't gate on fmt.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NLJYcEVmFmVgtyWFgtLjez


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
When a dependency declares another package from git, a URL or file:,
and the project's package.json "overrides" pins it back to a registry
version, npm installs the registry release. socket-patch still treated
that copy as installed from git: hosted scan skipped it and exited 0
with the package unpatched, vendored scan failed, and vex refused to
attest a correctly patched install.

The check that decides which lock entries come from the registry now
reads the root package.json overrides (top-level rules, rules nested
under the dependent or its ancestors, "." values and $name refs). An
override that clearly sends the edge to a registry spec makes the copy
patchable again in hosted mode, vendored mode and vex. Anything less
clear keeps the old, cautious behavior.

Fixes #490

Assisted-by: Claude Code:claude-opus-5-5
The hosted capstone suite gains the #490 project shape: a local
package depends on left-pad from git and the project's overrides pin
it to the registry release. The test checks that scan redirects it,
that a fresh npm ci installs the patched bytes, that vex verifies
them, and that rollback restores the lock. It skips on npm older
than 8.3, which has no overrides.

Refs #490

Assisted-by: Claude Code:claude-opus-5-5
npm before 8.3 has no overrides, so the #490 case doesn't exist there.
Return without calling skip(), which panics on the pinned (REQUIRED)
CI legs for npm 6 and 7.

Refs #490

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 1, 2026 19:03
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

npm 9.0.0 can't install the #490 project at all: it dies with
"Invalid comparator: github:..." when an override covers a git spec.
The case doesn't exist on that npm, so the e2e returns instead of
failing CI's pinned 9.0.0 leg. npm 9.9 and later run it fully.

Refs #490

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread crates/socket-patch-core/src/vendor/npm_origin.rs Outdated
Two override rules scoped to different ancestors at the same nesting
depth tied, and the one later in key order won. npm uses the rule of
the closest ancestor, so a farther registry rule could clear an edge
that a closer git or file: rule keeps on that source. socket-patch
could then patch or attest a copy npm still installs from the spec.

Rules are now ranked by how close their ancestor is to the
dependent, then by depth. Equally ranked rules that disagree count as
unclear, so the copy stays reported as unpatched.

Refs #490

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 c657128. Configure here.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 1, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Burn-down agent: ready for review at c657128 (c657128f2872ee00702ab5472c983dc28b75b8fc).

  • CI: all 490 check runs success or skipped on the head SHA, no failures or pending runs
  • Bugbot: reviewed c657128, no findings; 0 unresolved review threads
  • Mergeable, no conflicts
  • Reviewer note: Check the closest-override-wins rule in npm override classification.

Generated by Claude Code

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

npm hosted and vendored modes refuse a registry-installed package when its dependent's git spec is replaced by an overrides entry (regression from #345)

2 participants