[agent] Found by the scheduled vlt bug-hunt routine (ledger #307).
Summary
Say a vlt project vendors left-pad@1.3.0 (scan --mode vendored), and then the dependency leaves the lock in the usual way: it's bumped to left-pad@1.2.0 and vlt install runs, or vlt uninstall left-pad runs. After that, every socket-patch scan --mode vendored exits 1 with partial_failure / vendor_lock_entry_not_found and tells the user to "run vlt install first", which they already did.
The documented way out is scan --prune. It does revert the stale ledger entry (gc.revertedVendoredEntries: ["pkg:npm/left-pad@1.3.0"], and .socket/ is gone afterwards), but the same run still exits 1. It reports the purl it just reconciled as a failed vendor. Only the run after that one exits 0.
The cause: vendored_ledger_supplement adds every ledger entry that has no crawled counterpart back into discovery. The intent is "on a fresh clone the committed artifact IS the dependency", but the function never asks whether the lock still wires that artifact. The vendor step then tries to re-vendor a package the lock no longer has. It does this before the prune GC runs, which is the documented order.
Impact
- A scheduled
scan --mode vendored (a cron or CI job) goes red permanently after any routine dependency upgrade or removal. The error points users at the wrong fix (vlt install).
scan --mode vendored --prune, the documented reconcile, still fails the one run that actually reconciles. CI can't tell a real vendor failure from a successful cleanup.
- The human output is misleading too. It prints
installed content differs from patch baseline; the patched content will be vendored for left-pad@1.3.0, because the supplement points the path at node_modules/left-pad, which is now 1.2.0.
Repro (Linux, vlt 1.3.5, main 61cfb9b)
The local mock registry and patch API are the ones described in the ledger (#307). Any registry with left-pad@1.3.0 and @1.2.0 plus a patch for 1.3.0 works.
mkdir app && cd app
echo '{"config":{"registries":{"npm":"http://127.0.0.1:18555/"}}}' > vlt.json
echo '{"name":"app","version":"1.0.0","dependencies":{"left-pad":"1.3.0"}}' > package.json
vlt install --allow-scripts ':scripts'
socket-patch scan --mode vendored --yes # rc 0, vendored
echo '{"name":"app","version":"1.0.0","dependencies":{"left-pad":"1.2.0"}}' > package.json
vlt install --allow-scripts ':scripts' # lock now has only ~npm~left-pad@1.2.0
socket-patch scan --mode vendored --yes # rc 1 partial_failure vendor_lock_entry_not_found
socket-patch scan --mode vendored --prune --yes # rc 1 ...but gc.revertedVendoredEntries=[left-pad@1.3.0]
socket-patch scan --mode vendored --yes # rc 0
Human output of the --prune run:
pkg:npm/left-pad@1.3.0: installed content differs from patch baseline; the patched content will be vendored
Downloading 1 patch...
[error] pkg:npm/left-pad@1.3.0 (vendor_lock_entry_not_found): vlt-lock.json has no default-registry entry for left-pad@1.3.0; run `vlt install` first
Nothing was vendored: 1 patch failed (see above).
GC: reverted 1 vendored entry; swept 0 orphan vendor dirs.
rc 1
The vlt uninstall left-pad variant behaves the same way (rescan rc 1, prune rc 1 and reverts, next run rc 0).
Expected vs actual
- CLI_CONTRACT.md (scan → vendored): "
scan --prune reconciles ledger entries whose dependency left the lockfile", and the prune's leg (b) reverts "EVERY ledger entry whose dependency is no longer in the lockfile graph". A run that does exactly that should exit 0, perhaps with a warning. It shouldn't report partial_failure for the entry it reconciled.
- The ledger supplement is documented for the fresh-clone case, where the committed artifact satisfies the lock. When the lock no longer references the artifact, the entry isn't a discoverable dependency. A plain rescan should skip it with a warning (pointing at
--prune) rather than fail with advice to run vlt install.
- Actual: plain rescans fail with exit 1 until
--prune runs, and the --prune run itself fails with exit 1.
Matrix (Linux; macOS / Windows untested because probe branches are blocked this run)
| vlt |
bump → rescan |
bump → --prune |
prune reverts entry |
next run |
| 1.0.10 |
rc 1 |
rc 1 |
yes |
rc 0 |
| 1.2.0 |
rc 1 |
rc 1 |
yes |
rc 0 |
| 1.3.3 |
rc 1 |
rc 1 |
yes |
rc 0 |
| 1.3.5 |
rc 1 (×2) |
rc 1 (×2) |
yes |
rc 0 |
1.3.5, vlt uninstall |
rc 1 |
rc 1 |
yes |
rc 0 |
npm control on the same mock (package-lock v3): the bump → rescan also exits 1. --prune there doesn't revert at all (revertedVendoredEntries: []) and stays at rc 1, so npm users stay stuck. I've passed that to the npm routine's ledger separately. The supplement/ordering part is shared code.
First bad release: none. Release 4.0.0 predates vendored vlt support, so this is unreleased main behaviour.
Suspect code
crates/socket-patch-cli/src/commands/scan/discovery.rs:141: vendored_ledger_supplement supplements ledger entries without checking that the lock still wires them (dispatch_in_use_one, crates/socket-patch-cli/src/commands/vendor.rs:260, already answers Some(false) here; the prune GC uses it).
crates/socket-patch-cli/src/commands/scan/mod.rs:1626: the call site. The vendor step runs before the prune GC.
crates/socket-patch-core/src/vendor/vlt_lock.rs:475: the refusal and its misleading hint.
[agent] Found by the scheduled vlt bug-hunt routine (ledger #307).
Summary
Say a vlt project vendors
left-pad@1.3.0(scan --mode vendored), and then the dependency leaves the lock in the usual way: it's bumped toleft-pad@1.2.0andvlt installruns, orvlt uninstall left-padruns. After that, everysocket-patch scan --mode vendoredexits 1 withpartial_failure/vendor_lock_entry_not_foundand tells the user to "runvlt installfirst", which they already did.The documented way out is
scan --prune. It does revert the stale ledger entry (gc.revertedVendoredEntries: ["pkg:npm/left-pad@1.3.0"], and.socket/is gone afterwards), but the same run still exits 1. It reports the purl it just reconciled as a failed vendor. Only the run after that one exits 0.The cause:
vendored_ledger_supplementadds every ledger entry that has no crawled counterpart back into discovery. The intent is "on a fresh clone the committed artifact IS the dependency", but the function never asks whether the lock still wires that artifact. The vendor step then tries to re-vendor a package the lock no longer has. It does this before the prune GC runs, which is the documented order.Impact
scan --mode vendored(a cron or CI job) goes red permanently after any routine dependency upgrade or removal. The error points users at the wrong fix (vlt install).scan --mode vendored --prune, the documented reconcile, still fails the one run that actually reconciles. CI can't tell a real vendor failure from a successful cleanup.installed content differs from patch baseline; the patched content will be vendoredforleft-pad@1.3.0, because the supplement points the path atnode_modules/left-pad, which is now 1.2.0.Repro (Linux, vlt 1.3.5, main
61cfb9b)The local mock registry and patch API are the ones described in the ledger (#307). Any registry with
left-pad@1.3.0and@1.2.0plus a patch for 1.3.0 works.Human output of the
--prunerun:The
vlt uninstall left-padvariant behaves the same way (rescan rc 1, prune rc 1 and reverts, next run rc 0).Expected vs actual
scan --prunereconciles ledger entries whose dependency left the lockfile", and the prune's leg (b) reverts "EVERY ledger entry whose dependency is no longer in the lockfile graph". A run that does exactly that should exit 0, perhaps with a warning. It shouldn't reportpartial_failurefor the entry it reconciled.--prune) rather than fail with advice to runvlt install.--pruneruns, and the--prunerun itself fails with exit 1.Matrix (Linux; macOS / Windows untested because probe branches are blocked this run)
--prunevlt uninstallnpm control on the same mock (package-lock v3): the bump → rescan also exits 1.
--prunethere doesn't revert at all (revertedVendoredEntries: []) and stays at rc 1, so npm users stay stuck. I've passed that to the npm routine's ledger separately. The supplement/ordering part is shared code.First bad release: none. Release 4.0.0 predates vendored vlt support, so this is unreleased main behaviour.
Suspect code
crates/socket-patch-cli/src/commands/scan/discovery.rs:141:vendored_ledger_supplementsupplements ledger entries without checking that the lock still wires them (dispatch_in_use_one,crates/socket-patch-cli/src/commands/vendor.rs:260, already answersSome(false)here; the prune GC uses it).crates/socket-patch-cli/src/commands/scan/mod.rs:1626: the call site. The vendor step runs before the prune GC.crates/socket-patch-core/src/vendor/vlt_lock.rs:475: the refusal and its misleading hint.