Repository navigation
Vendored uv script locks: once the user deletes one wired script (or its .py.lock), vendor --revert, remove, rollback and the hosted takeover can never unwind the other scripts, and the takeover's suggested fix is the command that fails #890
Description
[agent] Found by the scheduled uv bug-hunt routine (ledger #310).
Summary
A vendored patch that is wired into several PEP 723 script locks (a.py + a.py.lock, b.py + b.py.lock, …) records one wiring record per file in a single ledger entry. If the user then deletes one of those scripts, which is a normal thing to do with throwaway uv scripts, every unwind of the whole entry hard-fails on the missing file:
Error: Failed to revert pkg:pypi/six@1.16.0: cannot read ./b.py.lock: No such file or directory (os error 2)
Reverted 0 vendored packages; 1 failed.
Nothing is reverted. The surviving a.py / a.py.lock stay wired to .socket/vendor/..., and the wheel and ledger entry stay too. The same thing happens with remove, with rollback, and with the vendored → hosted takeover. The takeover exits 0 with success, but its skip reason says NOT switched to hosted — run \socket-patch vendor --revert` to clean up, which is exactly the command that fails. The CLI offers no way out. The user has to restore the deleted script from git, run the revert, and delete it again, or edit .socket/vendor/state.json` by hand.
The PEP 751 lane goes through the same code path: with pylock.toml + pylock.dev.toml both vendored, deleting pylock.dev.toml makes vendor --revert fail with cannot read ./pylock.dev.toml.
Hosted mode handles the same deletion fine: rollback exits 0 and restores a.py / a.py.lock, because hosted pins live only in the files that still exist.
Impact
- The user can't leave vendored mode or switch to hosted for any remaining script until they recreate the deleted file. If the deletion was never committed, they can't recreate it from git at all.
- The takeover reports exit 0 /
successwhile leaving everything vendored, and the remedy it prints loops back to the failing command. vendor --checkstays green (exit 0) throughout, so CI doesn't flag the stuck state.
Repro (real uv, local mock patch server serving a patched six 1.16.0 wheel)
mkdir proj && cd proj && git init -q
for n in a b; do
cat > $n.py <<'EOF'
# /// script
# requires-python = ">=3.9"
# dependencies = ["six==1.16.0", "attrs>=20"]
# ///
import six
print(getattr(six, "SOCKET_PATCHED", 0))
EOF
uv lock -q --script $n.py
done
socket-patch scan --mode vendored --yes --json # success: a.py, a.py.lock, b.py, b.py.lock wired
rm b.py b.py.lock # the user drops one script (deleting only b.py or only b.py.lock does the same)
socket-patch vendor --revert --json; echo $? # 1, partialFailure, "cannot read ./b.py.lock: No such file or directory"
socket-patch remove pkg:pypi/six@1.16.0 --yes --json; echo $? # 1, status error, same message
socket-patch rollback --yes --json; echo $? # 1, partial_failure, same message
socket-patch scan --mode hosted --yes --json; echo $? # 0, success, skipped: "...cannot read ./b.py.lock...;
# NOT switched to hosted — run `socket-patch vendor --revert` to clean up"
grep -c socket/vendor a.py # 1: still wired
uv run --locked --script a.py # 1: still the vendored wheelExpected vs actual
- Expected: a record whose file no longer exists references nothing, so there's nothing left to restore in it. The revert should drop that record (with an advisory naming the file) and unwind the files that still exist. That's what hosted rollback effectively does, and it matches the CLI_CONTRACT rule that unwinds restore "the pristine registry line" for what is still wired. At minimum, the error should name a remedy that works, and the takeover shouldn't point at a command that can't succeed.
- Actual: one missing file fails the whole entry (
RevertOutcome::failed), for every unwind command, with a raw I/O error.
Matrix (main 9c43dfc)
| OS | uv | deleted | revert | remove | rollback | takeover |
|---|---|---|---|---|---|---|
| Linux | 0.8.17 | b.py + b.py.lock | fail (exit 1) | fail (exit 1) | fail (exit 1) | stays vendored, exit 0 |
| Linux | 0.12.23 | b.py + b.py.lock | fail | fail | fail | stays vendored, exit 0 |
| Linux | 0.12.23 | only b.py.lock / only b.py | fail / fail | – | – | – |
| Linux | 0.12.23 | pylock.dev.toml (pylock.toml + pylock.dev.toml) | fail | – | – | – |
| control | 0.8.17 / 0.12.23 | nothing deleted, two scripts | pass (byte-identical) | |||
| control (hosted) | 0.12.23 | b.py + b.py.lock | rollback pass | |||
| macOS / Windows | 0.5.31 / 0.12.23 | probe queued: https://github.com/SocketDev/socket-patch/actions/runs/37370890534 |
The failure comes from a file read, not from anything OS-specific. I'll add the probe results as a comment. I didn't bisect: the behavior looks present since multi-file script / pylock vendoring landed.
Suspect code
crates/socket-patch-core/src/vendor/pypi_lock.rs:770:revert_python_locksreturnsRevertOutcome::failed(error)on anyread_fileerror, includingNotFound, before it looks at the other records.crates/socket-patch-cli/src/commands/scan/hosted.rs:1917: the takeover skip text recommendsvendor --reverteven when that revert is the step that just failed.
Backlog review — 2026-10-08
Priority: P1 → P2. Deleting one wired uv script prevents unwinding others. Real recoverability issue with a user-deleted-file trigger; do not close it as noise.
Activity
mikolalysenko commentedon Oct 5, 2026
[agent] Probe run https://github.com/SocketDev/socket-patch/actions/runs/37370890534: on Windows × uv 0.12.23, all 12 cells reproduce (deleted
b.py+b.py.lock/ onlyb.py.lock/ onlyb.py, × revert / remove / rollback / takeover).cannot read .\\b.py.lock: The system cannot find the file specified.(or.\\b.py).success.a.pystays wired, anduv run --locked --script a.pystill runs the vendored wheel.The ubuntu and Windows 0.5.31 jobs ran in the same run; their logs are linked there. The macOS jobs were still queued when this run ended.
vendor --revert --dry-runpredicts the failure (exit 1, same message), andscan --mode hosted --dry-runreportsredirect_vendored_revert_failedwithredirected: 0. So the previews are consistent; the wet unwind just never gets past the deleted file.Generated by Claude Code
uv bug-hunt run 21: #890, #891; #723 / #767 still fail
mikolalysenko commentedon Oct 5, 2026
[agent] Triaged as
priority:p1(uv). It's a distinct cause: the vendored unwind of a multi-record PyPI ledger entry treats a wiring record whose file is gone as a hard error, when it should drop that record with an advisory. This is related to, but not the same as, #869 (a stale specifier written back on revert). No duplicate or open PR found.Generated by Claude Code
mikolalysenko commentedon Oct 6, 2026
[agent] macOS evidence from the run-21 probe, which finished after the issue was filed. On main
9c43dfc, macos-latest × uv 0.5.31 and 0.12.23 reproduce all 12 cells: deletingboth,lockorscriptcombined with each ofrevert,remove,rollbackandtakeover.vendor --revertexits 1 (partialFailure),removeexits 1 (error) androllbackexits 1 (partial_failure), all withcannot read ./b.py.lock(or./b.py).a.pystays wired and.socket/vendor/pypistays.scan --mode hosted(the V→H takeover) exits 0 withsuccess, buta.pyis still wired to the vendored wheel.Job: https://github.com/SocketDev/socket-patch/actions/runs/37370890534/job/111967435815
That makes the OS table Linux ✗, Windows ✗, macOS ✗.
Generated by Claude Code
mikolalysenko commentedon Oct 6, 2026
[agent] uv bug-hunt run 22: the same failure also hits the project lane, so this isn't limited to scripts. Repro on main
9c43dfc, Linux, uv 0.8.17 and 0.12.23, reproduced on both versions:uv.lock, for examplerm uv.lockbefore a relock, or a branch that drops it, with pyproject.toml still wired.After each of these, pyproject.toml still has
six = { path = ".socket/vendor/pypi/<uuid>/six-1.16.0-…whl" }and.socket/vendorstays, so the user can't remove the patch with any command. The takeover again exits 0 and recommends the revert that fails.Controls, all passing on both versions: restoring only
pyproject.toml(git checkout pyproject.toml) or onlyuv.lockand then running revert / remove / rollback / takeover → success, nothing left wired,.socket/vendorgone.Code:
crates/socket-patch-core/src/vendor/pypi_uv.rs:804returnsRevertOutcome::failed("cannot read uv.lock")before it touches pyproject.toml. The comment atcrates/socket-patch-core/src/vendor/pypi.rs:1570-1575already notes that "flavoruvwith uv.lock gone fails 'cannot read uv.lock'", but it guards onlyrepair-reconstructed entries with empty wiring, not a normal entry whose lock went missing. The fix for the script lane should cover this file as well: unwire what still exists and treat a missing wired file as already reverted.Generated by Claude Code
mikolalysenko commentedon Oct 11, 2026
[agent] Claiming this issue (with #1285; shared root cause:
revert_python_locksdecides wiring liveness for the whole ledger entry instead of per script/lock pair, so one removed or deleted pair blocks every other pair's unwind). Branch: agent/fix-pypi-lock-revert-per-pair. Claim-ID: 2026-10-11T15:20:53Z-b628b2Generated by Claude Code
mikolalysenko commentedon Oct 11, 2026
[agent] Draft PR: #1391
Generated by Claude Code
Revert each vendored uv script pair on its own