Skip to content

Fix yarn classic VEX for unlocked dep ranges (#1538) - #1548

Merged
Mikola Lysenko (mikolalysenko) merged 8 commits into
mainfrom
agent/fix-yarn-classic-vex-dep-ranges
Oct 14, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 8 commits into
mainfrom
agent/fix-yarn-classic-vex-dep-ranges

Conversation

@mikolalysenko

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

Copy link
Copy Markdown
Collaborator

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

Fixes #1538

Summary

A yarn.lock pin written by v4.0.0 can lock a patched package whose
patched package.json adds a dependency or changes a range the lock
never locked. Yarn 1 installs what the lock names, so the patched
package runs beside the old dependency. #1463 made the hosted rescan
flag such a pin, but three VEX paths still attested it not_affected.
They now omit it.

Root cause

Hosted yarn classic VEX never compared what the patched package.json
asks for with what the install or the lock actually provides:

  1. Installed vex / in-run scan --vex (changed range).
    vex/verify.rs unresolved_npm_dependency only checked that
    node_modules/<dep> existed. (The in-run path verifies like
    standalone vex for a pin the run reported redirect_unconfirmed,
    since refused uuids never enter the assumed-applied set, so it shared
    this gap.)
  2. Lock-only vex (added dependency) and 3. lock-only vex
    (changed range).
    The lockfile basis excused package_not_found
    without looking at the pinned block's dependencies: sub-map.

Fix

  • Installed: find the copy node actually loads (the nearest
    node_modules/<dep>) and require its version to satisfy the declared
    range (utils::npm_range, moved from the yarn berry backend so both
    callers share it). Installed optional dependencies are range-checked
    too. A dependency the project forces for this package (npm
    overrides, pnpm.overrides, yarn resolutions, either unscoped or
    scoped under the patched package itself; not * or empty) is not
    range-checked, because its version is the user's choice. Undecidable ranges or versions and
    non-registry specs are never evidence (same as before).
  • Lock-only: discovery keeps each hosted classic pin's block
    (PatchedRef::yarn_classic_block). When vex would attest from the
    lock alone and the patch changes package.json, it reads the patched
    package.json (.socket/blobs, else GET /patch/blob/<afterHash>)
    and requires the block's sub-maps to declare exactly those dependencies
    (compared by value, whichever way yarn quoted them), each one locked.
    Otherwise the patch is omitted:
    • dependency_not_installed when the sub-maps differ, or
    • new dependency_manifest_unverifiable when the patched
      package.json can't be read (--offline without the local blob, or
      a fetch failure). This fails closed and is documented in
      CLI_CONTRACT.md.

Per-issue checklist (#1538)

Path Test red → green
1. installed, changed range vex::verify::tests::unresolved_npm_dependency_checks_the_declared_range (unit); e2e_vex_lockfile yarn::hosted_patch_adding_an_uninstalled_dependency_is_omitted ("dependency out of range" cell, all 4 yarn flavors) unit: left: None, right: Some("is-number") before the fix, passes after
2. lock-only, added dependency e2e_vex_lockfile yarn::hosted_classic_lock_only_checks_the_pinned_dependency_maps ("added dependency") before: status: success, verified not_affected (exit 0); after: dependency_not_installed
3. lock-only, changed range same test ("changed range") same mechanism as 2; passes after
controls same test: a v5 pin (sub-map rewritten, descriptor locked) attests from the API blob and from .socket/blobs only; no blob anywhere gives dependency_manifest_unverifiable —

Test evidence (local, Linux, rust 1.93.1)

  • cargo fmt --all -- --check: clean
  • cargo clippy --workspace --all-features -- -D warnings: clean
  • cargo test -p socket-patch-cli --all-features --test e2e_vex_lockfile -- yarn::: 14/14 pass
  • cargo test --workspace --all-features --no-fail-fast: 246 test binaries pass. 10 tests fail only in this sandbox. They run as root, so the write-failure / unremovable-file / permission tests can't fail a write (in_process_redirect x3, repair x2, core copy_tree / vlt_heal / pypi_poetry / pypi_requirements write-failure tests), and e2e_bun_lockb::binary_shared_bundled_record_hosted_pin_is_managed needs patches-api.socket.dev, which the sandbox proxy blocks. None of them touch vex or yarn code; CI is the authority.
  • Wrappers (npm/, pypi/, gem/) only dispatch the binary, so they need no change.

Code review (/code-review high) findings not fixed

Review rounds

  • /code-review high: fixed overrides, optional deps, sub-map quoting
    (compared by value), blob path / hash validation, and the shared
    record-key helper; the two not fixed are explained above.
  • Bugbot / reviewer findings on scoped overrides (@types/x, nested npm
    parents, parent>dep, parent/dep, parent/**/dep, *, padded *)
    were all fixed, each with a unit case, and every thread is resolved.
    Bugbot's review of 2734608 found no issues.

Follow-ups

  • formats::yarn::classic_deps::dep_maps_match (used by the hosted and vendored writers) still compares lines with values always quoted, so it rewrites a block yarn wrote as foo latest needlessly. It's harmless (the rewrite is equivalent) and left out of this PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_014eFQuemHsEJCBj24DTCTeq


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
A hosted yarn classic pin written by v4 can install a patched package
whose package.json now asks for a newer dependency range (is-number
^7.0.0) while the lock still installs the old one (6.0.0). vex and
scan --vex only checked that node_modules/<dep> existed, so they
attested not_affected for a package that runs with the old dependency.

The installed check now finds the copy node actually loads (the
nearest node_modules/<dep>) and omits the patch with
dependency_not_installed when that copy's version is outside the
declared range. Ranges or versions the npm range reader can't decide,
and non-registry specs, are still never evidence.

The npm range reader moves from the yarn berry vendor backend to
utils::npm_range so both callers share it.

Refs #1538

Assisted-by: Claude Code:claude-opus-5-5
On a checkout with only package.json and yarn.lock, vex attested a
hosted yarn classic pin written by v4 even when the patched package
adds a dependency or bumps a range the lock never locked. Yarn 1
installs the dependencies the pinned block's sub-maps name, so such a
pin installs the patched package without what it now needs.

Discovery now keeps the pinned block of each hosted classic pin. When
a patch changes package.json and vex would attest it from the lock
alone, it reads the patched package.json (.socket/blobs, else the
patch API) and requires the block's dependency sub-maps to be exactly
the patched ones, each locked. Otherwise the patch is omitted with
dependency_not_installed, or dependency_manifest_unverifiable when the
patched package.json can't be read.

Fixes #1538

Assisted-by: Claude Code:claude-opus-5-5
- Skip the installed range check for a dependency the project forces
  with npm overrides, pnpm.overrides or yarn resolutions: its version
  is the user's choice, not a sign the patch is missing.
- Range-check installed optional dependencies too (absent ones are
  still fine), so installed and lock-only VEX agree.
- Compare a yarn classic block's sub-maps with the patched
  package.json by value, so `foo latest` (how yarn writes it) matches.
- Read the patched package.json blob from the manifest's .socket dir,
  accept only a 64-hex hash as its name, and check its size before
  reading it.
- Share the package.json record-key rule and compute the lock-only
  candidates once.

Refs #1538

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 14, 2026 21:31
@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/vex/verify.rs
The installed range check skipped any dependency an override key
merely mentioned: a nested npm override's parent (which only scopes
the overrides under it) counted as forced, and splitting keys on every
`/` made `@types/react` force `react`. Keys are now read with the yarn
selector parser (scope-aware), after pnpm's `>`, and a nested npm
override's parent counts only when its "." entry forces it.

Refs #1538

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/vex/verify.rs Outdated
Comment thread crates/socket-patch-core/src/vex/verify.rs Outdated
An npm override of "*" or "" (directly or as a nested override's "."
entry) keeps the declared range, so it doesn't force the dependency's
version and the installed copy is still range-checked. A "$name"
reference pins the root's own spec and still counts as forced.

Refs #1538

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/vex/verify.rs
An override scoped under another package (yarn "mkdirp/minimist",
pnpm "mkdirp>minimist", npm {"mkdirp": {"minimist": ...}}) doesn't
touch the patched package's own copy of the dependency, so it no
longer skips the range check. Overrides are read as (parent, target)
pairs and count only when unscoped or scoped under the patched
package itself. A whitespace-padded "*" is a no-op like "*".

Refs #1538

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/vex/verify.rs
A yarn resolution "mkdirp/**/minimist" only reaches minimist under
mkdirp, but the range check read the "**" as no parent and treated it
as forcing minimist everywhere. The parent is now the nearest segment
before the target that isn't "**"; only a leading "**/" applies
everywhere.

Refs #1538

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 2734608. Configure here.

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

Labels

None yet

Projects

None yet

3 participants