Repository navigation
Fix yarn classic VEX for unlocked dep ranges (#1538) - #1548
Merged
Mikola Lysenko (mikolalysenko) merged 8 commits intoOct 14, 2026
Merged
Conversation
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
Mikola Lysenko (mikolalysenko)
marked this pull request as ready for review
October 14, 2026 21:31
Collaborator
Author
|
BugBot review Generated by Claude Code |
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
Collaborator
Author
|
BugBot review Generated by Claude Code |
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
Collaborator
Author
|
BugBot review Generated by Claude Code |
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
Collaborator
Author
|
BugBot review Generated by Claude Code |
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
Collaborator
Author
|
BugBot review 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 2734608. Configure here.
Tanmay Singla (Tanmay182003)
approved these changes
Oct 14, 2026
Mikola Lysenko (mikolalysenko)
deleted the
agent/fix-yarn-classic-vex-dep-ranges
branch
October 14, 2026 23:56
This was referenced Oct 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.jsonadds a dependency or changes a range the locknever 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.jsonasks for with what the install or the lock actually provides:
vex/ in-runscan --vex(changed range).vex/verify.rsunresolved_npm_dependencyonly checked thatnode_modules/<dep>existed. (The in-run path verifies likestandalone
vexfor a pin the run reportedredirect_unconfirmed,since refused uuids never enter the assumed-applied set, so it shared
this gap.)
vex(added dependency) and 3. lock-onlyvex(changed range). The lockfile basis excused
package_not_foundwithout looking at the pinned block's
dependencies:sub-map.Fix
node_modules/<dep>) and require its version to satisfy the declaredrange (
utils::npm_range, moved from the yarn berry backend so bothcallers share it). Installed optional dependencies are range-checked
too. A dependency the project forces for this package (npm
overrides,pnpm.overrides, yarnresolutions, either unscoped orscoped under the patched package itself; not
*or empty) is notrange-checked, because its version is the user's choice. Undecidable ranges or versions and
non-registry specs are never evidence (same as before).
(
PatchedRef::yarn_classic_block). Whenvexwould attest from thelock alone and the patch changes
package.json, it reads the patchedpackage.json(.socket/blobs, elseGET /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_installedwhen the sub-maps differ, ordependency_manifest_unverifiablewhen the patchedpackage.jsoncan't be read (--offlinewithout the local blob, ora fetch failure). This fails closed and is documented in
CLI_CONTRACT.md.
Per-issue checklist (#1538)
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)left: None, right: Some("is-number")before the fix, passes aftere2e_vex_lockfile yarn::hosted_classic_lock_only_checks_the_pinned_dependency_maps("added dependency")status: success,verifiednot_affected (exit 0); after:dependency_not_installed.socket/blobsonly; no blob anywhere givesdependency_manifest_unverifiableTest evidence (local, Linux, rust 1.93.1)
cargo fmt --all -- --check: cleancargo clippy --workspace --all-features -- -D warnings: cleancargo test -p socket-patch-cli --all-features --test e2e_vex_lockfile -- yarn::: 14/14 passcargo 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_redirectx3,repairx2, corecopy_tree/vlt_heal/pypi_poetry/pypi_requirementswrite-failure tests), ande2e_bun_lockb::binary_shared_bundled_record_hosted_pin_is_managedneedspatches-api.socket.dev, which the sandbox proxy blocks. None of them touch vex or yarn code; CI is the authority.npm/,pypi/,gem/) only dispatch the binary, so they need no change.Code review (/code-review high) findings not fixed
--no-verifyskips the new lock-only gate. That matches Hosted yarn classic rescan and VEX accept a pin written by v4.0.0 whose patched package.json adds a dependency the lock doesn't have, so yarn installs the patched package without it #1462's installeddependency_not_installedcheck, which also runs only when verifying.--no-verifyis the documented trust-the-records mode.package.jsonand that aren't installed reach the fetch, so the set is tiny and sequential fetching is fine.Review rounds
(compared by value), blob path / hash validation, and the shared
record-key helper; the two not fixed are explained above.
@types/x, nested npmparents,
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 asfoo latestneedlessly. 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