[agent] Found by the scheduled Yarn classic (1.x) bug-hunt routine (ledger #304).
Summary
Vendor a yarn classic project, then drop one patched dependency with a plain yarn remove <pkg>, a routine dev change. Yarn deletes that package's yarn.lock block, so nothing references .socket/vendor/npm/<uuid>/ any more. socket-patch treats the missing block as drift (vendor_lock_entry_drifted: lock block \is-number@7.0.0` no longer exists; nothing to restore), keeps the artifact and the ledger entry (vendor_artifact_kept`), and then no command can get out of that state:
| command |
result |
exit |
rollback |
partial_failure, every time it runs |
1 |
vendor --revert |
success (yet the artifact and the ledger entry stay) |
0 |
remove pkg:npm/is-number@7.0.0 |
vendor_revert_kept: "re-run scan --mode vendored to normalize, then remove" |
1 |
scan --mode vendored (the remedy above), then remove |
nothing normalized; remove fails the same way |
1 |
repair |
success, no-op |
0 |
vendor --check |
vendor_check_ok for a package no longer in the lock |
0 |
The vendor_artifact_kept remedy ("undo the drift (restore the vendored lock entries or re-vendor) and re-run vendor --revert") can't be followed either: the dependency is gone, so there's nothing to re-vendor, and restoring the block means undoing the user's yarn remove. The only way out is deleting .socket/vendor/npm/<uuid> and editing state.json by hand.
Impact
socket-patch rollback exits 1 permanently in any vendored yarn classic project where a patched dependency was later removed, which breaks CI or uninstall scripts that run it.
- A stale vendored tarball stays committed forever.
vendor --check reports it as OK.
vendor --revert and rollback disagree about the same state (exit 0 vs exit 1).
Repro (yarn 1.22.22; local mock patch API serving is-number@7.0.0 and left-pad@1.3.0 patches)
mkdir p && cd p
echo '{"name":"p","version":"1.0.0","private":true,"dependencies":{"is-number":"7.0.0","left-pad":"1.3.0"}}' > package.json
yarn install
socket-patch scan --mode vendored --vendor-source service --api-url $API --org test-org --api-token x --json --yes # success
yarn remove is-number # yarn.lock no longer mentions the uuid
socket-patch rollback --json --yes; echo $? # partial_failure, 1 (vendor_lock_entry_drifted + vendor_artifact_kept)
socket-patch rollback --json --yes; echo $? # still 1
socket-patch scan --mode vendored --vendor-source service ... ; socket-patch remove pkg:npm/is-number@7.0.0 --json --yes; echo $? # vendor_revert_kept, 1
ls .socket/vendor/npm/1111*/ # is-number-7.0.0.tgz socket-patch.vendor.json (still there)
Expected vs actual
- Expected: a lock block that no longer exists isn't a third-party re-resolution that needs protecting. The artifact can't be "needed for a later restore", because nothing resolves through it. The yarn classic revert already has the probe for this (
lock_text_mentions_uuid, yarn_classic_lock.rs ~525). CLI_CONTRACT documents the same rule for the backends that can't tell drift apart: "a file that no longer references it is warned about and the artifact removed" (composer / maven / nuget; gem removes the artifact even when the lock is missing). The drift-keep rule ("fragments that no longer match — a user re-resolved — are left alone") is about a block that still exists with different content. When a block is gone, rollback / vendor --revert / remove should drop the ledger entry and the artifact, with a warning, once the lock no longer mentions the uuid. Failing that, the printed remedy must actually work.
- Actual: the artifact is kept forever,
rollback / remove exit 1 forever, vendor --revert exits 0 having done nothing, and the remedy loops.
Matrix (Linux, main 045d7ec, Node 22)
| yarn |
rollback after yarn remove |
scan --mode vendored + remove remedy |
stale artifact left |
| 1.7.0 |
exit 1 (x2) |
exit 1 |
yes |
| 1.22.22 |
exit 1 (x3, two separate projects) |
exit 1 |
yes |
Hosted mode passes the same flow: after yarn remove, rollback succeeds, the remaining block is restored and .socket/ is removed. Not OS-specific: the decision is made on lock text, so no probe run.
Suspect code
crates/socket-patch-core/src/vendor/yarn_classic_lock.rs:607: revert_recorded_block turns a missing block into vendor_lock_entry_drifted, so outcome.drift_skipped() (~505) returns early with keep_artifact before the lock_text_mentions_uuid probe gets a chance to show the artifact is unreferenced.
npm_lock.rs:1121, bun_lock.rs:1005 and pnpm_lock.rs:3112/3191/3255 emit the same "no longer exists; nothing to restore" drift, so npm uninstall / bun remove / pnpm remove are probably affected the same way (not verified here).
[agent] Found by the scheduled Yarn classic (1.x) bug-hunt routine (ledger #304).
Summary
Vendor a yarn classic project, then drop one patched dependency with a plain
yarn remove <pkg>, a routine dev change. Yarn deletes that package'syarn.lockblock, so nothing references.socket/vendor/npm/<uuid>/any more. socket-patch treats the missing block as drift (vendor_lock_entry_drifted: lock block \is-number@7.0.0` no longer exists; nothing to restore), keeps the artifact and the ledger entry (vendor_artifact_kept`), and then no command can get out of that state:rollbackpartial_failure, every time it runsvendor --revertsuccess(yet the artifact and the ledger entry stay)remove pkg:npm/is-number@7.0.0vendor_revert_kept: "re-runscan --mode vendoredto normalize, then remove"scan --mode vendored(the remedy above), thenremoveremovefails the same wayrepairsuccess, no-opvendor --checkvendor_check_okfor a package no longer in the lockThe
vendor_artifact_keptremedy ("undo the drift (restore the vendored lock entries or re-vendor) and re-runvendor --revert") can't be followed either: the dependency is gone, so there's nothing to re-vendor, and restoring the block means undoing the user'syarn remove. The only way out is deleting.socket/vendor/npm/<uuid>and editingstate.jsonby hand.Impact
socket-patch rollbackexits 1 permanently in any vendored yarn classic project where a patched dependency was later removed, which breaks CI or uninstall scripts that run it.vendor --checkreports it as OK.vendor --revertandrollbackdisagree about the same state (exit 0 vs exit 1).Repro (yarn 1.22.22; local mock patch API serving is-number@7.0.0 and left-pad@1.3.0 patches)
Expected vs actual
lock_text_mentions_uuid,yarn_classic_lock.rs~525). CLI_CONTRACT documents the same rule for the backends that can't tell drift apart: "a file that no longer references it is warned about and the artifact removed" (composer / maven / nuget; gem removes the artifact even when the lock is missing). The drift-keep rule ("fragments that no longer match — a user re-resolved — are left alone") is about a block that still exists with different content. When a block is gone, rollback /vendor --revert/removeshould drop the ledger entry and the artifact, with a warning, once the lock no longer mentions the uuid. Failing that, the printed remedy must actually work.rollback/removeexit 1 forever,vendor --revertexits 0 having done nothing, and the remedy loops.Matrix (Linux, main
045d7ec, Node 22)yarn removescan --mode vendored+removeremedyHosted mode passes the same flow: after
yarn remove,rollbacksucceeds, the remaining block is restored and.socket/is removed. Not OS-specific: the decision is made on lock text, so no probe run.Suspect code
crates/socket-patch-core/src/vendor/yarn_classic_lock.rs:607:revert_recorded_blockturns a missing block intovendor_lock_entry_drifted, sooutcome.drift_skipped()(~505) returns early withkeep_artifactbefore thelock_text_mentions_uuidprobe gets a chance to show the artifact is unreferenced.npm_lock.rs:1121,bun_lock.rs:1005andpnpm_lock.rs:3112/3191/3255emit the same "no longer exists; nothing to restore" drift, sonpm uninstall/bun remove/pnpm removeare probably affected the same way (not verified here).