Skip to content

Vendored uv: vendor --revert / remove / rollback delete the vendored wheel while a uv export-ed requirements.txt or pylock.toml still points at it (exit 0), so installs from the exported file fail #996

Description

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

Summary

After a uv project is vendored, uv export (requirements.txt, or --format pylock.toml) writes the vendored wheel path into the exported file. That's what uv does with a path source:

./.socket/vendor/pypi/<uuid>/six-1.16.0-py2.py3-none-any.whl \
    --hash=sha256:…

socket-patch already knows this file wires the patch: vex reports requirements.txt: pkg:pypi/six@1.16.0 is wired to Socket patch <uuid>, but uv.lock resolves the same version from elsewhere.

But vendor --revert, remove pkg:pypi/six@1.16.0 and rollback restore pyproject.toml / uv.lock byte-identically and then delete .socket/vendor/pypi/<uuid>/ with no warning, status: success, exit 0. The --dry-run revert previews the same clean removed. The exported file still names the deleted wheel, so every install from it fails.

The uv flavor revert (revert_uv) has no residual-reference check. The Python dispatcher already has a probe, unwired_pypi_reference_clause, that lists the root requirements.txt (plus -r includes), uv.lock, pylock*.toml and *.py.lock and refuses while any of them mentions the uuid dir. But it only runs for ledger entries with no wiring (guard_unwired_pypi_revert). An entry that does have wiring skips it.

Impact

A common uv workflow is to keep an exported requirements.txt / pylock.toml for Docker images, Dependabot or a pip-only deploy. After a revert or remove that reports success, uv pip install -r requirements.txt (and pip install -r, and uv pip sync pylock.toml) fails on every machine with Distribution not found at: file:///…/.socket/vendor/pypi/<uuid>/six-1.16.0-py2.py3-none-any.whl. The ledger entry is gone too, so repair can't bring the wheel back. The user has to restore it from git or re-export.

Repro (Linux, main 9c43dfc, CPython 3.11)

This uses the repo's own tests/prebuilt_common::Server as the vendoring service (SOCKET_VENDOR_URL) and a staged .socket/manifest.json + blob, the same as e2e_vendor_pypi_build.rs::stage_patch.

printf '[project]\nname = "app"\nversion = "0.1.0"\nrequires-python = ">=3.9"\ndependencies = ["six==1.16.0", "certifi"]\n' > pyproject.toml
uv lock && uv sync
# stage .socket/manifest.json + blob for pkg:pypi/six@1.16.0 (six.py + marker)
socket-patch vendor --json                     # exit 0, applied 1
uv export --frozen --no-emit-project -o requirements.txt   # six line = ./.socket/vendor/pypi/<uuid>/six-…whl --hash=…
uv venv fresh && uv pip install -p fresh/bin/python -r requirements.txt   # control: installs the patched six (SOCKET_PATCHED == 1)

socket-patch vendor --revert --json            # exit 0, status success, events: [removed]; no warning
#   pyproject.toml + uv.lock byte-identical to pre-vendor; .socket/vendor/ deleted
grep socket/vendor requirements.txt            # still ./.socket/vendor/pypi/<uuid>/six-1.16.0-py2.py3-none-any.whl
uv venv fresh2 && uv pip install -p fresh2/bin/python -r requirements.txt
# error: Distribution not found at: file:///…/.socket/vendor/pypi/<uuid>/six-1.16.0-py2.py3-none-any.whl

socket-patch remove pkg:pypi/six@1.16.0 --yes (events vendor_reverted, removed) and socket-patch rollback --yes give the same result. So does uv export --format pylock.toml -o pylock.toml (uv ≥ 0.8) followed by uv pip install -r pylock.toml.

Expected vs actual

  • docs/testing/uv-compatibility.md (Limits): "vendor --revert refuses to delete a vendored Python wheel while uv.lock, a PEP 751 lock, a script, or requirements.txt still references it…". The guard's own doc comment in vendor/pypi.rs gives the reason: deleting the artifact "while uv.lock, the pylock, the script, or requirements.txt still resolve through the vendored wheel — every later --frozen / --offline install fails".
  • CLI_CONTRACT (vendor revert): composer / maven / nuget "keep the artifact exactly while the live … still names its .socket/vendor/<eco>/<uuid> dir — a file that no longer references it is warned about and the artifact removed"; the requirements flavor keeps it on vendor_revert_residual_reference.
  • Expected: after restoring the uv pair, the revert keeps the artifact and the entry while another project file still references the uuid dir, with a warning naming the file (e.g. vendor_revert_residual_reference / vendor_artifact_kept), or at least warns. The dry run previews that.
  • Actual: the artifact is deleted silently, exit 0, and the exported file is left pointing at nothing.

OS × version

OS uv exported file vendor --revert remove rollback
Linux 0.5.31 requirements.txt reproduces reproduces reproduces
Linux 0.8.17 requirements.txt / pylock.toml reproduces / reproduces – –
Linux 0.12.23 requirements.txt (×2) / pylock.toml reproduces / reproduces reproduces reproduces
macOS / Windows – not probed: the missing check is in pure Rust revert logic with no OS-specific path, and uv fails on a missing local wheel on every OS

First bad version

Not bisected. revert_uv has never swept for references outside its own wiring records.

Suspect code

  • crates/socket-patch-core/src/vendor/pypi_uv.rs:1013-1050: the end of revert_uv writes the pair and returns kept_artifact: false without checking other files for the uuid dir.
  • crates/socket-patch-core/src/vendor/pypi.rs:1596 / 1412-1440: guard_unwired_pypi_revert → unwired_pypi_reference_clause already enumerates exactly the right files (root requirements.txt + includes, uv.lock, pylock*.toml, *.py.lock), but it's gated on the entry having no wiring. Running the same probe after a successful wired flavor revert (pypi.rs:~1620, next to the vendor_revert_residual_reference keep) would cover this.
  • Related but distinct: Vendored requirements.txt: vendor --revert / remove delete the vendored wheel while a -r include still points at it (exit 0), so every later pip install -r requirements.txt fails #867 (pip flavor: the residual sweep in revert_requirements misses -r includes). This one is the uv flavor, which has no sweep at all, and the trigger is uv's own uv export.

No probe runs: Linux only (see the table).

Activity

  1. mikolalysenko commented on Oct 7, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] More information (same run, main 9c43dfc, uv 0.12.23): the PEP 723 script lane has the same problem. I vendored a s.py + s.py.lock (six==1.16.0), then ran uv export --script s.py --frozen -o requirements.txt, which references ./.socket/vendor/pypi/<uuid>/six-…whl. vendor --revert then exits 0 (removed), restores s.py / s.py.lock byte-identically, and deletes .socket/vendor/, leaving requirements.txt pointing at a missing wheel. So the fix needs to cover the script-lock revert path too, not just revert_uv.


    Generated by Claude Code

  2. mikolalysenko commented on Oct 7, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged as priority:p1 (uv). Not a duplicate. Confirmed on main 9c43dfc: revert_pypi_opts (crates/socket-patch-core/src/vendor/pypi.rs) runs the in-use probe unwired_pypi_reference_clause only for ledger entries with no wiring. A wired flavor revert (revert_uv, the script-lock revert_python_locks, and so on) goes straight to remove_tree_and_prune with no check of the other project files.

    Shares root cause with #867: a wired PyPI revert deletes .socket/vendor/pypi/<uuid>/ without first probing every Python project file for the uuid dir. Will be fixed together.


    Generated by Claude Code

  3. mikolalysenko commented on Oct 7, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (with #867; shared root cause: a wired PyPI vendor revert deletes the vendored wheel without probing every Python project file for the uuid dir). Branch: agent/fix-pypi-revert-residual-probe. Claim-ID: 2026-10-07T09:20:53Z-6884c3


    Generated by Claude Code

  4. mikolalysenko commented on Oct 7, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft PR: #997


    Generated by Claude Code

  5. mikolalysenko commented on Oct 7, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Additional shape from the scheduled uv bug-hunt routine (ledger #310): the superseding re-vendor path hits the same missing residual-reference sweep. On main it can't happen yet, because the re-vendor refuses (pypi_uv_source_already_exists, #742). It appears once PR #943 (the vendored half of #742) lands. I tested #943's head 13247c5.

    # uv project: six==1.16.0, idna==3.7; mock patch server serves six at uuid aaaaaaaa-…01
    socket-patch scan --mode vendored --yes --json        # wires .socket/vendor/pypi/aaaaaaaa-…01/six-1.16.0-py2.py3-none-any.whl
    uv export --no-hashes --format requirements-txt -o requirements.txt   # names the aaaaaaaa-…01 wheel
    # the server now offers a superseding patch bbbbbbbb-…09
    socket-patch scan --mode vendored --yes --json        # exit 0, success
    #   applied; removed vendor_stale_artifact_removed "previous patch uuid's vendored artifact removed"
    uv pip install -r requirements.txt
    #   error: Distribution not found at: file:///…/.socket/vendor/pypi/aaaaaaaa-…01/six-1.16.0-py2.py3-none-any.whl
    uv re-vendor exit pyproject / uv.lock uv sync --locked exported requirements.txt
    0.5.31 0, success, no warning uuid bbbbbbbb-…09 ✅ patched (gen 2) ❌ names the deleted aaaaaaaa-…01 wheel
    0.8.17 same same ✅ ❌
    0.12.23 same same ✅ ❌

    The stale-artifact removal after a superseding re-vendor should get the same reference check (or a warning naming the file) as the fix for the revert / remove / rollback paths above. Otherwise #943 opens a second route to this failure. Linux only. The paths are plain relative file paths, nothing OS-specific.


    Generated by Claude Code

  6. mikolalysenko commented on Oct 7, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Re-checked on main 05ecc6e, which includes #1015 (scan every vendored-write file for vendored references). This still reproduces: #1015 widened the sweep to the registry's vendored-write rows, but a file that only uv export wrote is still not consulted.

    On uv 0.5.31, 0.8.17 and 0.12.23 (Linux), I ran scan --mode vendored --yes on a six==1.16.0 uv project, then wrote the export as req.txt, requirements.txt and pylock.toml (uv export --frozen [--format pylock.toml]). After that, vendor --revert --json exits 0 success with no warning, .socket/vendor/pypi/ is empty, and uv pip install -r req.txt fails with Distribution not found at: file:///…/.socket/vendor/pypi/aaaaaaaa-…/six-1.16.0-py2.py3-none-any.whl. It reproduced twice per version.

    For contrast, every ledger-less case passes on the same commit: with .socket/vendor/state.json deleted, revert reports vendor_orphan_still_wired and keeps the wheel for uv.lock, script locks, pylock.toml, pylock.dev.toml and pylock.my-env.toml. The gap is only the residual-reference check after a ledger-backed revert.


    Generated by Claude Code

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