Repository navigation
Vendored uv script lock can't be reverted after uv add --script adds an unrelated dependency (vendor_lock_entry_drifted, still exit 0) #474
Description
[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 --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).- 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 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).
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 # PATCHEDIsolation, 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 --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. - Actual: the whole revert is skipped and the artifact is kept.
vendor --revertstill exits 0 withsuccess.
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)
Activity
mikolalysenko commentedon Oct 1, 2026
[agent] Triaged as
priority:p1(uv). No duplicate or open fix PR found. Distinct from #473 (that one is the hosted upstream restore inredirect/upstream/uv.rs; this one is the vendored three-way merge invendor/pypi_lock.rs).Generated by Claude Code
mikolalysenko commentedon Oct 1, 2026
[agent] Shares root cause with #385: the vendored revert's three-way TOML merge (
restore_documentincrates/socket-patch-core/src/vendor/pypi_lock.rs) matches array elements by position and treats any array-length change as drift, and applies the lock-packagename/versionidentity check to every table (including pyproject[project]). Will be fixed together.Generated by Claude Code
mikolalysenko commentedon Oct 1, 2026
[agent] Claiming this issue (with #385; shared root cause: positional/length-strict array merge and table-level identity check in
vendor::pypi_lock::restore_document). Branch: agent/fix-pypi-toml-merge-identity. Claim-ID: 2026-10-01T16:20:43Z-576d58Generated by Claude Code
mikolalysenko commentedon Oct 1, 2026
[agent] Draft PR: #481
Generated by Claude Code
Let vendored Python reverts keep sibling edits
Test real uv/Hatch reverts after sibling edits
Fix vendored Python reverts refusing sibling edits (#385, #474) (#481)