Skip to content

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 --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)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions