[agent] Found by the scheduled uv bug-hunt routine (ledger #310).
Summary
After vendoring a PEP 723 script (tool.py + tool.py.lock), adding any unrelated dependency with uv add --script tool.py <pkg> makes vendor --revert and rollback keep the vendored wiring for good. They report vendor_lock_entry_drifted, but the patched package's own lock entry didn't change. uv only added one line to the script lock's [manifest] requirements array.
The revert's three-way merge in vendor/pypi_lock.rs treats any length change in a recorded array as drift. The script lock's [manifest] requirements is recorded as a whole array (it holds six's path entry), so a new requirement from the user counts as a conflicting edit. The same flow on a project uv.lock (uv add idna after vendoring) reverts cleanly.
Impact
vendor --revert exits 0 with status: success, but the script and lock still point at .socket/vendor/…, and .socket/vendor/pypi/<uuid> and the ledger entry are kept. uv run --locked --script tool.py keeps running the patched wheel.
rollback exits 1 (partial_failure, vendoredKept: lockfile wiring drifted).
- The advice "undo the drift (restore the vendored lock entries or re-vendor) and re-run
vendor --revert" can only be followed by dropping the user's new dependency. Re-vendoring doesn't help: scan --mode vendored reports already_vendored, and the next vendor --revert drifts again (tested).
Repro (Linux, main 6e7ef74)
Same local mock patch API as the other uv issues (embedded in the probe workflow below).
cat > tool.py <<'PY'
# /// script
# requires-python = ">=3.9"
# dependencies = ["six==1.16.0", "python-dateutil==2.8.2"]
# ///
import six; print("PATCHED" if getattr(six, "SOCKET_PATCHED", 0) else "PRISTINE")
PY
uv lock --script tool.py && git init -q && git add -A && git commit -qm init
socket-patch scan --mode vendored --json --yes $API # applied; uv run --locked --script tool.py → PATCHED
uv add --script tool.py idna==3.7 # only adds idna to [manifest] requirements + a [[package]]
socket-patch vendor --revert --json --yes $API # exit 0, success, events: vendor_lock_entry_drifted, vendor_artifact_kept, vendor_revert_kept
grep -c socket/vendor tool.py tool.py.lock # 1 / 2: still vendored
uv run --locked --script tool.py # PATCHED
Isolation, starting from the vendored state, with one edit each:
| Edit after vendoring |
Revert |
one { name = "idna", specifier = "==3.7" } line added to [manifest] requirements (by hand) |
❌ drifted, kept |
script metadata dependencies reflowed to multi-line (what uv add also does) |
✅ restored |
six's wheels = [{…}] reflowed to multi-line |
✅ restored (key order not byte-identical; uv accepts it) |
bare uv lock --script tool.py |
✅ (uv leaves the lock untouched) |
project uv.lock + uv add idna==3.7 (control) |
✅ restored |
Expected vs actual
- Expected: CLI_CONTRACT.md: "
vendor --revert restores the originals (fragments that no longer match — a user re-resolved — are left alone with a vendor_lock_entry_drifted warning …)". Nobody re-resolved six here, and its [[package]] entry and [manifest] element are exactly what vendoring wrote. Only a sibling element was added, so the revert should put six's path element back to its specifier form and leave idna alone, as the project-lock backend does.
- Actual: the whole revert is skipped and the artifact is kept.
vendor --revert still exits 0 with success.
OS × uv matrix
|
uv 0.5.17 (first uv lock --script) |
uv 0.5.31 |
uv 0.8.17 |
uv 0.12.21 |
| Linux (sandbox) |
❌ |
❌ |
❌ |
❌ |
| ubuntu-latest (probe) |
– |
❌ |
– |
❌ |
| macos-latest (probe) |
– |
❌ |
– |
❌ |
| windows-latest (probe) |
– |
❌ |
– |
❌ |
First bad
Not bisected. The only published release (4.0.0) predates the v5 vendoring consolidation (#277), so it isn't a usable comparison point.
Suspect code
crates/socket-patch-core/src/vendor/pypi_lock.rs:478 (restore_value: if live.len() != new.len() || original.len() != new.len() { return true; }), reached through restore_document for the lock file in revert_python_locks (pypi_lock.rs:600-615). Array elements should be matched by identity (name, plus marker/extras where needed), not by position and length. Line 513 has the same length check for arrays of tables.
Probe: https://github.com/SocketDev/socket-patch/actions/runs/36878632878 (ubuntu, macos, windows × uv 0.5.31 / 0.12.21)
[agent] Found by the scheduled uv bug-hunt routine (ledger #310).
Summary
After vendoring a PEP 723 script (
tool.py+tool.py.lock), adding any unrelated dependency withuv add --script tool.py <pkg>makesvendor --revertandrollbackkeep the vendored wiring for good. They reportvendor_lock_entry_drifted, but the patched package's own lock entry didn't change. uv only added one line to the script lock's[manifest] requirementsarray.The revert's three-way merge in
vendor/pypi_lock.rstreats any length change in a recorded array as drift. The script lock's[manifest] requirementsis recorded as a whole array (it holds six'spathentry), so a new requirement from the user counts as a conflicting edit. The same flow on a projectuv.lock(uv add idnaafter vendoring) reverts cleanly.Impact
vendor --revertexits 0 withstatus: success, but the script and lock still point at.socket/vendor/…, and.socket/vendor/pypi/<uuid>and the ledger entry are kept.uv run --locked --script tool.pykeeps running the patched wheel.rollbackexits 1 (partial_failure,vendoredKept: lockfile wiring drifted).vendor --revert" can only be followed by dropping the user's new dependency. Re-vendoring doesn't help:scan --mode vendoredreportsalready_vendored, and the nextvendor --revertdrifts again (tested).Repro (Linux, main 6e7ef74)
Same local mock patch API as the other uv issues (embedded in the probe workflow below).
Isolation, starting from the vendored state, with one edit each:
{ name = "idna", specifier = "==3.7" }line added to[manifest] requirements(by hand)dependenciesreflowed to multi-line (whatuv addalso does)wheels = [{…}]reflowed to multi-lineuv lock --script tool.pyuv.lock+uv add idna==3.7(control)Expected vs actual
vendor --revertrestores the originals (fragments that no longer match — a user re-resolved — are left alone with avendor_lock_entry_driftedwarning …)". Nobody re-resolved six here, and its[[package]]entry and[manifest]element are exactly what vendoring wrote. Only a sibling element was added, so the revert should put six'spathelement back to itsspecifierform and leaveidnaalone, as the project-lock backend does.vendor --revertstill exits 0 withsuccess.OS × uv matrix
uv lock --script)First bad
Not bisected. The only published release (4.0.0) predates the v5 vendoring consolidation (#277), so it isn't a usable comparison point.
Suspect code
crates/socket-patch-core/src/vendor/pypi_lock.rs:478(restore_value:if live.len() != new.len() || original.len() != new.len() { return true; }), reached throughrestore_documentfor the lock file inrevert_python_locks(pypi_lock.rs:600-615). Array elements should be matched by identity (name, plus marker/extras where needed), not by position and length. Line 513 has the same length check for arrays of tables.Probe: https://github.com/SocketDev/socket-patch/actions/runs/36878632878 (ubuntu, macos, windows × uv 0.5.31 / 0.12.21)