Skip to content

Vendored uv script locks: after uv remove --script drops the package from one of two vendored scripts, every unwind keeps the other script wired (revert exits 0, remove/rollback exit 1, hosted takeover refuses) #1285

Description

[agent] Found by the scheduled uv bug-hunt routine (ledger #310).

Summary

#1231 fixed #1214 for a single vendored PEP 723 script: after uv remove --script s.py six, the revert now classifies the script's records as vendor_lock_entry_removed and finishes. It does that only when no wired file names the entry's uuid. With two vendored scripts that share the same six entry, removing six from just one of them (uv remove --script b.py six) leaves a.py still naming the uuid. So b.py / b.py.lock (which no longer mention the uuid anywhere) are classified as drift again, and the drift gate blocks the whole unwind, including a.py, which the user never touched.

I first reported this shape on #1214 before #1231 merged (#1214 (comment)). #1214 is now closed, and the shape still reproduces on main, so I'm filing it separately.

Impact

  • vendor --revert and scan --prune exit 0 success with removed 0. a.py and a.py.lock stay wired to .socket/vendor/pypi/<uuid>/, and uv run --script a.py keeps running the vendored wheel.
  • remove pkg:pypi/six@1.16.0 exits 1 vendor_revert_kept, and rollback exits 1 (vendor_lock_entry_drifted + vendor_artifact_kept).
  • scan --mode hosted exits 0 with redirect_vendored_revert_failed ("part of its vendored wiring was edited since vendoring … NOT switched to hosted"), and a.py stays vendored.
  • The only remedy the messages name ("undo the drift … and re-run vendor --revert") is to add six back to b.py, which the user deliberately removed. Nothing installs unpatched and vendor --check stays green, so this is a stuck unwind, not a silent unpatch.

Repro (Linux, main e9be746, uv 0.5.31 and 0.12.24)

Patch data came from a local mock of the patch API (six@1.16.0), the same one used for earlier uv issues.

git init -q app && cd app
for s in a.py b.py; do
cat > $s <<'EOF'
# /// script
# requires-python = ">=3.9"
# dependencies = [
#     "six==1.16.0",
#     "attrs>=20",
# ]
# ///
import six
EOF
uv lock -q --script $s
done
git add -A && git commit -qm init
socket-patch scan --mode vendored --yes --json   # exit 0; a.py, a.py.lock, b.py, b.py.lock wired
uv remove --script b.py six
grep -c socket a.py a.py.lock b.py b.py.lock     # a.py:1 a.py.lock:2 b.py:0 b.py.lock:0
socket-patch vendor --revert --json; echo $?     # 0 success, removed 0:
#   vendor_lock_entry_drifted "b.py.lock changed since vendoring; conflicting fields were preserved"
#   vendor_artifact_kept, vendor_revert_kept
grep -c socket a.py                              # 1 — still wired
socket-patch remove pkg:pypi/six@1.16.0 --yes; echo $?   # 1 vendor_revert_kept
socket-patch rollback --yes; echo $?                     # 1 vendor_lock_entry_drifted + vendor_artifact_kept

Control: the same two-script fixture without the uv remove reverts cleanly (exit 0, both scripts byte-identical to upstream, .socket/vendor/pypi gone). The single-script uv remove --script shape from #1214 also passes on this main.

Expected vs actual

  • Expected: what Fix uv script lock revert stuck as drift (#1214) #1231 does for one script, applied per script/lock pair. b.py / b.py.lock no longer name the uuid, so their records are "removed" (nothing to restore). a.py / a.py.lock are unchanged, so they're restored to upstream, and the revert finishes. CLI_CONTRACT.md says scan --prune / vendor --revert revert vendored entries whose dependency is gone, and Fix uv script lock revert stuck as drift (#1214) #1231's description treats "nothing references the vendored wheel" as removed, not drift.
  • Actual: b.py's records are classified as drift because a.py still names the uuid, and the drift keeps the whole entry, a.py included.

Matrix (Linux, real uv lock --script / uv remove --script, fresh fixture per cell, each run twice)

uv unwind after uv remove --script b.py six exit a.py after wheel
0.5.31 vendor --revert 0 (drift) still wired kept
0.5.31 scan --prune --mode vendored 0 still wired kept
0.5.31 remove pkg:pypi/six@1.16.0 1 vendor_revert_kept still wired kept
0.5.31 rollback 1 still wired kept
0.5.31 scan --mode hosted 0 redirect_vendored_revert_failed still vendored kept
0.12.24 all five, same as above same same same
0.12.24 control: no uv remove, vendor --revert 0 restored removed

macOS / Windows not probed: the logic is a uuid text check with no OS-specific branch.

Suspect code

crates/socket-patch-core/src/vendor/pypi_lock.rs:737-754 (revert_python_locks): unreferenced is one flag for the whole entry. It's set to false as soon as any wired file still contains the uuid, so the vendor_lock_entry_removed arm at ~line 776 never fires for b.py's records while a.py is still wired. Deciding "removed" per script/lock pair (does this record's own file, or its paired script/lock, still name the uuid?) would let the unchanged pair restore and the removed pair converge, while a real hand edit inside a still-referencing file stays drift. Related, but a different trigger: #890 (a wired script or .py.lock deleted outright blocks the other scripts' unwind).

Activity

  1. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    v5 triage: P2, not a release blocker. P2 for unwind after one of two vendored PEP 723 scripts removes a shared package. Keep the genuine cleanup bug, but do not let the expanding multi-script state matrix gate the ordinary project-lockfile release.

    This follows the maintainer's release scope: one normally completing CLI instance, prioritizing valid-lockfile patch/install behavior, compatibility, and actionable CLI UX.

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

    agent:triagedbugSomething isn't workingbughuntFound by a scheduled package-manager bug-hunt agentpm:uvuvpriority:p2uxCLI commands, help, diagnostics, output consistency, or actionable recovery instructions.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions