Skip to content

Fix yarn classic VEX ignoring bundled copies (#758) - #1712

Merged
Mikola Lysenko (mikolalysenko) merged 3 commits into
mainfrom
agent/fix-yarn-classic-bundled-copies
Oct 26, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 3 commits into
mainfrom
agent/fix-yarn-classic-bundled-copies

Conversation

@mikolalysenko

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

Copy link
Copy Markdown
Collaborator

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

Fixes #758

Root cause

A yarn classic project can install the same name@version twice: once
from its yarn.lock block, and once unpacked from another package's
tarball because that package lists it in bundledDependencies.
yarn.lock records nothing about the bundled copy. Hosted, vendored and
lockfile VEX discovery all read only yarn.lock for yarn classic, so none
of them can see the bundled copy: the scan wires the lock block, says
nothing, and the OpenVEX document says not_affected while
node_modules/<parent>/node_modules/<pkg> ships unpatched.

vlt had the same gap (#471) and npm/pnpm/bun read it from their locks.

Fix

  • New vendor::yarn_classic_bundled::bundled_copies reads bundled copies
    from the installed node_modules: every package whose package.json
    declares bundleDependencies / bundledDependencies (array, true
    or object form), each bundled package under its own node_modules,
    the dependencies of those found beside them, and everything nested
    under any of them. Packages yarn placed in a parent's node_modules
    from the lock (the parent bundles nothing) are not bundled copies.
    Symlinks are never followed, and the walk is capped like vlt's.
  • Yarn classic lockfile discovery records each bundled copy as an
    unpatched copy beside the parent's lock entry (only when yarn.lock
    holds a Socket wiring, so unpatched projects pay nothing). That reuses
    the existing same-lock contest: the ref is withheld from attestation
    but still managed (rollback/remove still work), and the hosted engine's
    existing redirect_copy_unpatched warning names the copy and keeps the
    uuid out of the in-run VEX's assume_applied.
  • Vendored yarn classic warns vendor_bundled_instance_skipped, as vlt
    does. When the only installed copy is a bundled one, vendoring refuses
    with vendor_lock_entry_not_rewritable naming it (not "run yarn install").
  • The walk also covers each workspace member's own node_modules
    (reached through the member's link in the root node_modules; links
    leaving the project are never followed), and counts a bundled
    package's peers as bundled (npm 7+ packs them).
  • redirect_copy_unpatched names every bundled copy a parent ships
    (it was de-duplicated per lock entry), and for a bundled copy tells
    the user to vendor or update the bundling package instead of the
    generic "depend on the registry release".
  • CLI_CONTRACT.md and docs/ecosystems.md describe the new detection and
    its limit: a lock-only vex with no node_modules can't see the copy
    (yarn.lock has no trace of it).

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

Tests (red → green)

Issue path Test Before fix After fix
#758 lockfile VEX (hosted and vendored refs; named, true, transitive and nested bundle shapes; other version and non-bundled controls) vex::discover::yarn::tests::classic_installed_bundled_copy_contests_the_ref FAILED (ref attested) ok
#758 vendored scan names the copy (control: parent bundles nothing) vendor::yarn_classic_lock::tests::issue_758_bundled_copy_is_named no warning ok
#758 bundle shapes: peer of a bundled package, workspace member node_modules, link outside the project vendor::yarn_classic_bundled::tests::* n/a (new module) ok
#758 only a bundled copy installed → vendor_lock_entry_not_rewritable vendor::yarn_classic_lock::tests::issue_758_only_a_bundled_copy_is_refused_as_not_rewritable n/a ok
#758 hosted scan with real yarn 1.22.22: copy named, in-run VEX and post-reinstall vex don't attest e2e_redirect_yarn_classic_build::classic_bundled_copy_is_named_and_not_attested n/a (new) ok

Commands run locally:

  • cargo fmt --all -- --check: clean
  • cargo clippy --workspace --all-features -- -D warnings: clean
  • cargo test -p socket-patch-core --all-features --lib -- vex::discover vendor::yarn vendor::vlt_bundled hosted::: 738 passed
  • SOCKET_PATCH_YARN_CLASSIC_E2E_VERSION=1.22.22 SOCKET_PATCH_YARN_E2E_REQUIRED=1 cargo test -p socket-patch-cli --all-features --test e2e_redirect_yarn_classic_build classic_bundled_copy: 1 passed
  • cargo test --workspace --all-features --no-fail-fast (debuginfo off
    to fit the sandbox disk): every suite passes except 11 tests that
    fail here for environment reasons, none touching yarn: 8
    permission-denial tests (the sandbox runs as root, so read-only dirs
    and files stay writable: copy_tree, vlt_heal, pypi_poetry,
    pypi_requirements, in_process_redirect write-failure and vlt-heal
    cases, two repair cases), one e2e_bun_lockb test that needs a live
    blob download, and one that pins a bun lock literal different from the
    local bun 1.4.4. CI runs these as a normal user.

CI and Bugbot

CI is green on 44f1a19 (38 ran, including yarn-classic 1.22.22, which runs
the new e2e; 50 skipped by path filters). Bugbot reviewed 44f1a19 and
found no issues.

Review

/code-review high ran on the diff. Fixed: workspace member
node_modules, peer deps in the bundle, per-copy warning de-dup, the
bundled-copy remedy text, and the only-bundled vendored refusal code.
Left as known limits (see below): the walk cap and the lack of a cache
across discovery passes. Also noted: a bundled copy also counts as
evidence against another lock (package-lock / pnpm) wiring the same
version, but only when yarn.lock has some Socket wiring, since the walk
is gated on that.

The e2e returns early on yarn 1.0–1.6, which install nothing from a
file: tarball (the same limit the suite already handles elsewhere).

Known limits, not fixed here:

  • The walk is capped at 50,000 node_modules entries (as vlt's is); a
    tree past that may miss a copy.
  • The walk isn't cached across discovery passes in one run; it only runs
    when yarn.lock holds a Socket wiring.

Note on cost: when yarn.lock has a Socket wiring, discovery now reads
each installed package's package.json once per discovery pass. That's
the only way to find a bundle, since yarn.lock doesn't record one.


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
yarn 1 unpacks a package's bundleDependencies from its tarball into
node_modules/<parent>/node_modules and yarn.lock records nothing about
them. A hosted or vendored scan wired the lock block, said nothing, and
the VEX document claimed not_affected while the bundled copy shipped
unpatched.

When yarn.lock holds a Socket wiring, read the installed node_modules
for bundled copies (each named bundle entry, its dependencies beside
it, and everything nested under them). Lockfile discovery records each
as an unpatched copy beside the parent's lock entry, so `vex` and the
in-run VEX withhold the attestation and hosted scans name the copy with
redirect_copy_unpatched. Vendored scans warn with
vendor_bundled_instance_skipped, as they do for vlt.

Fixes #758

Assisted-by: Claude Code:claude-opus-5-5
Follow-ups from review of the bundled-copy fix:

- Walk each workspace member's own node_modules too, reached through
  its link in the root node_modules, so a bundle yarn couldn't hoist
  is still found. Links leaving the project are never followed.
- Count a bundled package's peers as part of the bundle, as npm 7+
  packs them.
- Name every bundled copy a parent ships, not just the first one: the
  hosted warning was de-duplicated per lock entry.
- Tell the user to vendor or update the bundling package, not to
  "depend on the registry release", which they already do.
- When the only installed copy is bundled, vendoring now refuses with
  vendor_lock_entry_not_rewritable naming it instead of suggesting
  `yarn install`.

Refs #758

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

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

1 similar comment
@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 44f1a19. Configure here.

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

Copy link
Copy Markdown
Collaborator Author

Labeled Ready for review by the burn-down agent.

  • Head: 44f1a19cc8491931bd94394a45e0e6573379a306
  • CI: 38/38 non-skipped checks green (50 skipped), no main-wide failures
  • Bugbot: reviewed 44f1a19 — no new issues; no unresolved review threads
  • Mergeable against main, no CHANGELOG.md change
  • Reviewer focus: the node_modules walk in vendor/yarn_classic_bundled.rs (bundled-copy detection, symlink skipping, walk cap) and how bundled copies feed the existing same-lock contest in yarn classic VEX discovery.

Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Final review brief

What it does. yarn classic unpacks a package's bundleDependencies into node_modules/<parent>/node_modules/<name> and records nothing about them in yarn.lock, so hosted, vendored and VEX discovery could attest not_affected while an unpatched bundled copy shipped. This adds a walk of the installed node_modules (no symlinks followed, capped at 50k entries) that finds bundled copies, makes them contest the ref in lockfile VEX (same path as npm's inBundle copies), and warns or refuses in vendored mode. Fixes #758.

Risk: low–medium. The new code runs only for yarn classic projects whose yarn.lock already holds a Socket wiring (VEX) or while vendoring a yarn classic package. Its failure mode is conservative: a wrongly flagged copy withholds attestation rather than over-attesting. Cost: a vendored run walks the tree once per vendored package, the same pattern vlt already uses.

Look here

  • yarn_classic_bundled.rs:64:`` the walk that classifies bundled copies, and bundle_closure below it
  • yarn_classic_bundled.rs:149:``` workspace-member links; links that leave the project are dropped through canonicalize + `strip_prefix`
  • vex/discover/yarn.rs:169: VEX contest, gated on a Socket wiring in yarn.lock
  • yarn_classic_lock.rs:125:```` vendored warning, and the not_found → `not_rewritable` refusal when only a bundled copy is installed
  • hosted/engine.rs:1965: per-copy remedy text in redirect_copy_unpatched

Verified

Changes I made: none.

Open questions (non-blocking): Discovery::bundled_copy (mod.rs:1036) sets the remedy through unpatched_copies.last_mut(). With purl: None, unpatched_copy returns early, so the remedy would land on an earlier, unrelated copy. The only caller always passes Some, so nothing goes wrong today. Taking a String instead would rule it out. This can be a follow-up.

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


Generated by Claude Code

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

4 participants