[agent] Found by the scheduled Yarn classic (1.x) bug-hunt routine (ledger #304).
Summary
This is a common team flow. Two branches each run scan --mode vendored for a different package. Git merges yarn.lock cleanly (the blocks are different), but .socket/vendor/state.json conflicts (AA, both sides add an entry under entries). The obvious resolution is git checkout --ours or --theirs. It leaves the other branch's package wired in yarn.lock to its committed tarball, with no ledger entry. socket-patch handles that state inconsistently:
vendor --check exits 0 ("committed artifact and wiring verified") and audits only the remaining ledger entry. It also exits 0 with no events once the ledger is gone entirely, while yarn.lock still resolves into .socket/vendor/.
rollback exits 0: "Reverted vendoring for pkg:npm/left-pad@1.3.0", with no event or warning for the unrecorded wiring. It then deletes state.json, so is-number stays wired and its tarball stays committed, with no ledger at all.
- After that, a second
rollback fails: "the vendor ledger is missing — restore .socket/vendor/state.json from version control". So does scan --mode vendored ("the lockfile references this patch without its vendor ledger entry; restore .socket/vendor/state.json from version control"). No commit in history has a ledger with the is-number entry next to the merged lock, so that remedy doesn't apply. The only exits are hand-editing state.json or git checkout -- yarn.lock.
remove pkg:npm/is-number@7.0.0 exits 1 not_found. vendor --revert exits 0 and skips it with vendor_orphan_still_wired.
repair already detects this exact shape: exit 1, vendor_ledger_missing, "a lockfile references .socket/vendor/npm// but the vendor ledger has no entry for it". So does scan. Only the CI gate (vendor --check) and rollback miss it.
Impact
vendor --check is the documented CI gate, and it stays green on a project whose ledger no longer covers a wired package. The next rollback reports success but leaves that package patched and committed. It also destroys the ledger that the remaining entries' originals lived in, and every later rollback or re-vendor then fails with an unusable remedy.
- Users read "rollback succeeded" as "nothing is vendored any more". yarn's frozen install keeps installing the patched is-number from the orphaned tarball.
Repro (yarn 1.22.22; local mock patch API serving left-pad@1.3.0 and is-number@7.0.0)
git init -q app && cd app
echo '{"name":"app","version":"1.0.0","private":true,"dependencies":{"left-pad":"1.3.0","is-number":"7.0.0"}}' > package.json
yarn install && echo node_modules > .gitignore && git add -A && git commit -qm base
git checkout -qb a && socket-patch scan --mode vendored --yes --package pkg:npm/left-pad@1.3.0 && git add -A && git commit -qm a
git checkout -q main && git checkout -qb b && socket-patch scan --mode vendored --yes --package pkg:npm/is-number@7.0.0 && git add -A && git commit -qm b
git checkout -q a && git merge --no-edit b # CONFLICT (add/add) in .socket/vendor/state.json; yarn.lock auto-merges
git checkout --ours .socket/vendor/state.json && git add -A && git commit -qm merge
socket-patch vendor --check; echo $? # 0 — "pkg:npm/left-pad@1.3.0: committed artifact and wiring verified"
socket-patch repair; echo $? # 1 — vendor_ledger_missing: "a lockfile references .socket/vendor/npm/2222…/ but the vendor ledger … has no entry"
socket-patch rollback; echo $? # 0 — "Reverted vendoring for pkg:npm/left-pad@1.3.0"
ls .socket/vendor/state.json # gone
grep -A2 '^is-number' yarn.lock # resolved "file:./.socket/vendor/npm/2222…/is-number-7.0.0.tgz#…"
socket-patch vendor --check; echo $? # 0 — no output at all
socket-patch rollback; echo $? # 1 — "vendor ledger is missing — restore .socket/vendor/state.json from version control"
rm -rf node_modules && yarn install --frozen-lockfile # installs the patched is-number from the orphaned tarball
A manual JSON union of both sides' entries works end to end: vendor --check verifies both, and rollback is byte-exact to the base lock. So the bug is the handling of the one-sided resolution, not the merge itself.
Expected vs actual
- CLI_CONTRACT "Vendored JVM support (v5)", the
vendor --check paragraph: "Missing ledger entries fail with vendor_ledger_missing." Actual: a lockfile reference with no ledger entry passes vendor --check (exit 0). For JVM, run_check already refuses "artifacts exist without a vendor ledger" (vendor.rs:983-993); npm has no equivalent.
- CLI_CONTRACT rollback "State discovery": "A project whose lockfiles still reference
.socket/vendor/ artifacts but whose vendor ledger is missing errors asking for .socket/vendor/state.json to be restored". Actual: with a partial ledger, rollback exits 0, skips the unrecorded reference silently, and deletes the ledger, which creates exactly the state the contract refuses.
- Expected:
vendor --check fails (vendor_ledger_missing, naming the referenced uuid/path, as repair does). rollback either refuses before writing or warns and keeps the ledger. The remedy text should cover the merge case (merge both sides' entries).
OS × version
| OS |
yarn |
result |
| Linux |
1.7.0 |
reproduces (×2) |
| Linux |
1.10.1 |
reproduces (×2) |
| Linux |
1.22.22 |
reproduces (×2) |
This is ledger and lock-scan logic with no OS-specific paths, so macOS and Windows weren't probed. It is likely npm-family-wide (the check loop is ecosystem-generic), but it was only measured with yarn classic.
First bad: not a regression. v4.0.0 has no vendor --check, and its rollback skips vendored entries ("use remove or vendor --revert"). Tested on main 9c43dfc.
Suspect code
crates/socket-patch-cli/src/commands/vendor.rs:974 run_check: iterates only state.entries and manifest keys, and never calls vendored_backend::repair::scan_vendor_references (which repair uses at vendored_backend/repair.rs:585 to emit vendor_ledger_missing for uncovered references).
crates/socket-patch-cli/src/commands/rollback.rs:~1100: the scan_vendor_references guard runs only when the ledger is absent, not for references the present ledger doesn't cover.
[agent] Found by the scheduled Yarn classic (1.x) bug-hunt routine (ledger #304).
Summary
This is a common team flow. Two branches each run
scan --mode vendoredfor a different package. Git mergesyarn.lockcleanly (the blocks are different), but.socket/vendor/state.jsonconflicts (AA, both sides add an entry underentries). The obvious resolution isgit checkout --oursor--theirs. It leaves the other branch's package wired in yarn.lock to its committed tarball, with no ledger entry. socket-patch handles that state inconsistently:vendor --checkexits 0 ("committed artifact and wiring verified") and audits only the remaining ledger entry. It also exits 0 with no events once the ledger is gone entirely, while yarn.lock still resolves into.socket/vendor/.rollbackexits 0: "Reverted vendoring for pkg:npm/left-pad@1.3.0", with no event or warning for the unrecorded wiring. It then deletes state.json, so is-number stays wired and its tarball stays committed, with no ledger at all.rollbackfails: "the vendor ledger is missing — restore .socket/vendor/state.json from version control". So doesscan --mode vendored("the lockfile references this patch without its vendor ledger entry; restore .socket/vendor/state.json from version control"). No commit in history has a ledger with the is-number entry next to the merged lock, so that remedy doesn't apply. The only exits are hand-editing state.json orgit checkout -- yarn.lock.remove pkg:npm/is-number@7.0.0exits 1not_found.vendor --revertexits 0 and skips it withvendor_orphan_still_wired.repairalready detects this exact shape: exit 1,vendor_ledger_missing, "a lockfile references .socket/vendor/npm// but the vendor ledger has no entry for it". So doesscan. Only the CI gate (vendor --check) androllbackmiss it.Impact
vendor --checkis the documented CI gate, and it stays green on a project whose ledger no longer covers a wired package. The nextrollbackreports success but leaves that package patched and committed. It also destroys the ledger that the remaining entries' originals lived in, and every later rollback or re-vendor then fails with an unusable remedy.Repro (yarn 1.22.22; local mock patch API serving left-pad@1.3.0 and is-number@7.0.0)
A manual JSON union of both sides'
entriesworks end to end:vendor --checkverifies both, androllbackis byte-exact to the base lock. So the bug is the handling of the one-sided resolution, not the merge itself.Expected vs actual
vendor --checkparagraph: "Missing ledger entries fail withvendor_ledger_missing." Actual: a lockfile reference with no ledger entry passesvendor --check(exit 0). For JVM,run_checkalready refuses "artifacts exist without a vendor ledger" (vendor.rs:983-993); npm has no equivalent..socket/vendor/artifacts but whose vendor ledger is missing errors asking for.socket/vendor/state.jsonto be restored". Actual: with a partial ledger, rollback exits 0, skips the unrecorded reference silently, and deletes the ledger, which creates exactly the state the contract refuses.vendor --checkfails (vendor_ledger_missing, naming the referenced uuid/path, asrepairdoes).rollbackeither refuses before writing or warns and keeps the ledger. The remedy text should cover the merge case (merge both sides'entries).OS × version
This is ledger and lock-scan logic with no OS-specific paths, so macOS and Windows weren't probed. It is likely npm-family-wide (the check loop is ecosystem-generic), but it was only measured with yarn classic.
First bad: not a regression. v4.0.0 has no
vendor --check, and itsrollbackskips vendored entries ("useremoveorvendor --revert"). Tested on main9c43dfc.Suspect code
crates/socket-patch-cli/src/commands/vendor.rs:974run_check: iterates onlystate.entriesand manifest keys, and never callsvendored_backend::repair::scan_vendor_references(whichrepairuses atvendored_backend/repair.rs:585to emitvendor_ledger_missingfor uncovered references).crates/socket-patch-cli/src/commands/rollback.rs:~1100: thescan_vendor_referencesguard runs only when the ledger is absent, not for references the present ledger doesn't cover.