Skip to content

Fix npm root-bundled deps refused (#1618) - #1661

Merged
Mikola Lysenko (mikolalysenko) merged 5 commits into
mainfrom
agent/fix-npm-root-bundled-entries
Oct 22, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 5 commits into
mainfrom
agent/fix-npm-root-bundled-entries

Conversation

@mikolalysenko

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

Copy link
Copy Markdown
Collaborator

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

Fixes #1618

Summary

npm 7+ writes inBundle: true on every package the root project's own bundleDependencies covers, and on their dependencies. socket-patch read that flag as "ships inside a dependency's tarball", so for those packages:

  • hosted scan skipped them (redirect_npm_bundled_instance_skipped, exit 0) and pointed at a "bundling parent" that does not exist. On a v2 lock only the legacy mirror was rewired, leaving the lock half-wired;
  • vendored scan / vendor refused them (vendor_lock_entry_not_rewritable, exit 1 on every run);
  • vex left them out.

Root bundles have no parent tarball. npm install / npm ci fetch them from resolved + integrity like any other entry (the issue shows npm ci installing a hand-rewired root-bundled entry on npm 8, 10 and 12).

Root cause and fix

Every hosted, vendored and VEX reader keys off one flag, NpmLockEntry::bundled, which walk_npm_packages (vendor/lock_inventory/npm.rs) took straight from inBundle. The fix is at that shared boundary: npm_dependency_bundled walks up the entry's owners (the key before its last node_modules/):

  • An owner that is a plain dependency, or that declares its own bundleDependencies, is a dependency whose tarball carries the copy, so the copy stays bundled.
  • A chain that reaches the root "" is the project's own bundle when the lock's "" entry records a bundle. The copy then becomes rewirable.
  • Anything the walk can't place stays bundled, the conservative reading: a missing owner, a workspace member, or a root with no recorded bundle.

I checked the inputs against real npm 10.9.4 here. npm records the root bundle on packages[""].bundleDependencies as a list for all three package.json spellings (a list, true, and the bundledDependencies alias). It writes no inBundle for a workspace member's bundle, and no legacy bundled flag for a root bundle. So the legacy dependencies tree keeps its existing reading.

Known conservative gap: take a copy nested beneath a root-bundled package that itself declares bundleDependencies (a version-conflict nest). The lock can't tell whether that package's own tarball carries the copy, so it stays bundled, as it was before this PR. CLI_CONTRACT.md documents this.

Tests (red → green)

With the fix reverted to the raw inBundle flag, all four new core tests fail; with the fix they pass.

Issue symptom Test
classification (root bundle, transitive, nested, scoped; dependency bundles and unknown owners kept) vendor::lock_inventory::npm::tests::only_dependency_bundles_are_bundled
hosted skip + v2 half-wired mirror patch::redirect::tests::npm_root_bundled_entries_are_rewired
vendored exit 1 refusal vendor::npm_lock::tests::root_bundled_instance_is_vendored
VEX omission vex::discover::npm::tests::root_bundled_wired_entry_is_a_ref
end to end: hosted scan + in-run --vex in_process_redirect::scan_redirect_npm_root_bundled_entry_is_rewired_and_attested

Local runs (Linux, as root):

  • cargo fmt --all -- --check: clean.
  • cargo clippy --workspace --all-features -- -D warnings: clean.
  • cargo test -p socket-patch-core --all-features --lib: 6595 passed, 4 failed. The four are permission-dependent tests that can't make a write fail when run as root (copy_tree::relax_loop_must_not_traverse_symlinked_root, vlt_heal::an_unremovable_hidden_lock_keeps_every_store_entry, pypi_poetry::wire_write_failure_*, pypi_requirements::wire_failure_rolls_back_*). None of them touches npm code.
  • in_process_redirect: 145 passed, 3 failed, the same root-permission write-failure cases. in_process_vendor: 134 passed. hosted_inventory: 21 passed.
  • e2e_redirect_npm_build --include-ignored with real npm 10.9.4: 36 passed.
  • The full cargo test --workspace run hit the session's disk limit while linking the e2e test binaries, so I ran the suites above instead.

I didn't add a real-npm npm ci e2e for the root bundle. The issue already verifies that npm honours the rewired entry, and the in-process test covers the CLI path. The npm, pypi and gem wrappers only dispatch to the binary, so they need no change.

https://claude.ai/code/session_01RYRA3DFNMzVU2tcjyH1HVf


Note

Medium Risk
Touches shared npm lock classification used by redirect, vendor, and VEX; misclassification could skip rewires or attest wrong installs, though conservative fallbacks remain for ambiguous cases.

Overview
Fixes #1618: npm lock handling no longer treats every inBundle: true entry as “inside a dependency tarball.” Packages covered by the root project’s bundleDependencies (and their transitive deps) are installed from resolved/integrity like normal lock entries, so hosted redirect, vendoring, and VEX discovery can wire and attest them again.

The behavior change is centralized in npm_dependency_bundled: when walking package-lock.json packages, it walks up owner keys and only marks copies as bundled when a dependency (not the root bundle) carries them. Hosted scan, vendor, and VEX all share NpmLockEntry::bundled, so they pick up the same classification. CLI_CONTRACT.md documents the split between root bundles vs dependency inBundle/bundled skips.

Regression coverage adds unit tests for classification, redirect rewrite, vendoring, and VEX refs, plus an in-process test that hosted scan + in-run --vex attests a root-bundled package.

Reviewed by Cursor Bugbot for commit 89b80fd. Configure here.


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
npm 7+ marks every package covered by the root project's own
bundleDependencies (and their dependencies) with inBundle: true.
socket-patch read that flag as "ships inside a dependency's tarball",
so hosted scans skipped those packages, vendored scans failed with
exit 1, and VEX left them out, all while npm ci installs them from
the lock like any other entry.

Only a copy a dependency bundles is now treated as bundled: the lock
inventory walks up the entry's owners and stops at the first plain
dependency (or one that declares its own bundle). An owner chain that
reaches the root or a workspace member is the project's own bundle.
The legacy dependencies tree follows the same rule.

Fixes #1618

Assisted-by: Claude Code:claude-opus-5-5
Covers the three package.json spellings npm accepts for a root
bundle and checks the in-run VEX attests the rewired package.

Refs #1618

Assisted-by: Claude Code:claude-opus-5-5
npm writes no bundled flag in the legacy dependencies tree for a
root bundle, so that tree keeps reading bundled: true as before; only
the packages map needs the owner walk. Update the inventory fixture
that used a top-level inBundle entry as a stand-in for a dependency's
bundle, and re-bless the npm lock rewrite golden: its generator puts
inBundle on top-level entries, which are now rewired as root bundles.

Refs #1618

Assisted-by: Claude Code:claude-opus-5-5
Real npm records the root's bundle list on the lock's "" entry (as a
list, whatever spelling package.json used) and writes no inBundle for
a workspace member's bundle. So a top-level inBundle copy now counts
as the project's own bundle only when that list exists; a workspace
owner, a missing owner or a root without a bundle keeps the copy
bundled. The npm lock rewrite golden is back to main's version.

Also fix the docs this left stale, and move a test that had split
the #325 regression test from its doc comment.

Refs #1618

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 22, 2026 01:18
@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 89b80fd. Configure here.

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

Copy link
Copy Markdown
Collaborator Author

Ready for review at 89b80fd.

  • CI: 36/36 green (50 skipped), no main-wide failures.
  • Bugbot: reviewed 89b80fd, no findings.
  • Mergeable, no conflicts; CHANGELOG.md untouched.
  • Worth a reviewer's look: the owner walk in npm_dependency_bundled (vendor/lock_inventory/npm.rs) and the documented conservative gap for copies nested under a root-bundled package that declares its own bundleDependencies.

Slack announcement not sent: this run had no Slack send tool.


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Final review brief

What it does. npm writes inBundle: true both on copies a dependency ships inside its own tarball and on copies the root project's bundleDependencies covers. socket-patch treated both as unreachable, so root-bundled deps were skipped by hosted scan, refused by vendor (exit 1) and left out of VEX. The fix changes the one shared flag (NpmLockEntry::bundled): it walks up the entry's owners and clears the flag only when the chain reaches a root that records a bundle. Every other case stays bundled, as before.

Risk: low–medium. The flag feeds hosted, vendored and VEX, but it has a single reader (I grepped for "inBundle"; there is no other production reader). Every case the walk can't place keeps the old conservative behaviour (missing owner, workspace member, root with no recorded bundle, a root-bundled package with its own bundle).

Look here

  • npm_dependency_bundled owner walk:`` the whole behaviour change
  • call site
  • classification test:```` root, transitive, scoped, workspace, dependency bundle, orphan, root with no bundle
  • hosted v2 rewire test and end-to-end scan + --vex

Verified

  • Read the full diff (7 files, +372/−11). Traced the walk by hand for hoisted, nested, scoped, workspace-member and orphan keys.
  • declares_bundled_deps accepts both spellings and the bool, array and object forms. npm's lock copies bundleDependencies onto dependency entries, so the "owner declares its own bundle" branch can fire.
  • The legacy v1 bundled reading is unchanged. CHANGELOG.md is untouched and CLI_CONTRACT.md is updated to match.
  • Ran cargo test -p socket-patch-core --all-features --lib -- bundle locally: 109 passed.
  • CI: ci-ok and clippy green on 89b80fd. Bugbot reviewed 89b80fd with no findings. Mergeable, no open threads.

Changes I made: none.

Open questions: none blocking. One gap is documented: a copy nested under a root-bundled package that declares its own bundle stays skipped, which is the same as before this PR.

Auto-merge is armed: approving sends this straight to the merge queue.


Generated by Claude Code

Merged via the queue into main with commit 07d74fe Oct 22, 2026
86 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the agent/fix-npm-root-bundled-entries branch October 22, 2026 17:15
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

3 participants