Repository navigation
Vendored PyPI revert, remove, rollback and the hosted takeover delete the vendored wheel while a root uv export -o requirements.lock (Rye-style name) still installs from it (exit 0) #1252
Description
[agent] Found by the scheduled uv bug-hunt routine (ledger #310).
Summary
The vendored PyPI residual-reference probe (pypi_reference_clause) only reads files whose names end in .txt, plus fixed lock names and pylock*.toml / *.py.lock. A requirements file exported under a different extension is never probed. The common case is requirements.lock / requirements-dev.lock: Rye's lock names, which uv export -o requirements.lock and uv pip compile -o requirements.lock keep writing after a Rye → uv migration. So vendor --revert, remove, rollback and the vendored → hosted takeover delete .socket/vendor/pypi/<uuid>/ while that file still points at the wheel. They exit 0 with status: success and no vendor_revert_residual_reference keep, and the export can't be installed any more.
This is the same failure class as #1167 (subdirectory *.txt, fixed by #1168) and #1213 (subdirectory pylock.toml, PR #1223). It has a separate cause, though: the file is at the root, and it's skipped only because of its extension. I checked PR #1223 (b1f945f): it fixes the subdirectory pylock shapes but not this one.
Impact
- A Docker or CI image that runs
pip install -r requirements.lock/uv pip sync requirements.lockafter the unwind fails with "Distribution not found at: file:///…/.socket/vendor/pypi//six-1.16.0-py2.py3-none-any.whl". - The dry run (
vendor --revert --dry-run) previews a clean success, so nothing warns the user beforehand.
Repro (Linux, main e03a666)
mkdir proj && cd proj && git init -q
cat > pyproject.toml <<'EOF'
[project]
name = "demo"
version = "0.1.0"
requires-python = ">=3.9"
dependencies = ["six==1.16.0", "attrs>=20"]
EOF
uv lock && uv sync && git add -A && git commit -qm init
socket-patch scan --mode vendored --yes # six vendored, uv.lock wired
uv export --no-hashes --no-emit-project -o requirements.lock
grep socket/vendor requirements.lock # six @ file:///…/.socket/vendor/pypi/<uuid>/six-1.16.0-…whl
socket-patch vendor --revert --json # exit 0, status success, no residual keep
ls .socket/vendor/pypi # gone
uv venv v2 && VIRTUAL_ENV=$PWD/v2 uv pip install -r requirements.lock
# error: Distribution not found at: file:///…/.socket/vendor/pypi/<uuid>/six-1.16.0-py2.py3-none-any.whlThe same happens with remove pkg:pypi/six@1.16.0 --yes, rollback --yes and scan --mode hosted --yes (the takeover), and with requirements-dev.lock and deploy/requirements.lock. Each cell ran at least twice, against a local mock patch API that serves a real patched wheel.
Control: the same export written to requirements.txt is kept: the wheel stays, and the install still works.
Expected vs actual
- Expected: CLI_CONTRACT's vendored revert rules, and the Fix PyPI revert deleting still-referenced wheel (#996, #867) #997 / Vendored PyPI revert,
removeand the hosted takeover still delete the vendored wheel while a requirements file in a subdirectory (requirements/dev.txt,pip freeze > requirements/lock.txt) installs from it (exit 0), so that install then fails #1167 behaviour, say an artifact that a project file still installs from is kept withvendor_revert_residual_reference. Thepypi_reference_clausedoc comment lists "auv export -otarget" as something it must protect.uv export -oaccepts any file name, and Rye'srequirements.lockis the most common non-.txtname. - Actual: the wheel is deleted with exit 0 because the file name doesn't end in
.txt.
Matrix (Linux; the path logic has no OS-specific branch)
| uv | revert | remove | rollback | hosted takeover | dry run |
|---|---|---|---|---|---|
| 0.5.31 | deleted, exit 0 | deleted, exit 0 | – | – | – |
| 0.8.24 | deleted, exit 0 | deleted, exit 0 | deleted, exit 0 | deleted, exit 0 | success, no warning |
| 0.12.24 | deleted, exit 0 | deleted, exit 0 | deleted, exit 0 | deleted, exit 0 | success, no warning |
| 0.12.24 + PR #1223 | deleted | – | – | deleted | – |
0.12.24, requirements.txt control |
kept | kept (exit 1, #1184 wording) | – | – | – |
Related observation
On the scan side, a pre-existing requirements.lock exported from the unpatched uv.lock gets no pypi_multiple_lockfiles / #612 warning, and vendor --check exits 0. The same file named requirements.txt makes the check exit 1. I haven't filed that separately, because socket-patch doesn't claim requirements.lock as an input. It's the same name filter, though.
Suspect code
crates/socket-patch-core/src/vendor/pypi.rs:2069: the root listing skips anything that's neitheris_python_lock_namenor*.txt.crates/socket-patch-core/src/vendor/pypi.rs:2189:subdir_txt_nameshas the same*.txtfilter.
A possible direction: probe any regular root-level file whose bytes contain the .socket/vendor/pypi/<uuid>/ needle (the needle is unique), or at least the requirements*.lock / *.lock names. I haven't filed a fix.
Activity
mikolalysenko commentedon Oct 9, 2026
[agent] Claiming this issue (shared root cause: the vendored PyPI residual-reference probe only reads
*.txtand fixed lock names, so arequirements.lock-style export is never checked). Branch: agent/fix-pypi-residual-probe-lock-names. Claim-ID: 2026-10-09T12:21:38Z-442164Generated by Claude Code
mikolalysenko commentedon Oct 9, 2026
[agent] Draft fix PR: #1265
Generated by Claude Code
Test revert keeps wheels a requirements.lock uses
Keep vendored wheels a requirements.lock uses
mikolalysenko commentedon Oct 9, 2026
[agent] uv bug-hunt run 36 (ledger #310): I checked PR #1265 (
415ce51) against maine9be746on Linux with uv 0.12.24. Each cell was a fresh vendored uv project followed byuv export --frozen --no-hashes -o <file>, then the unwind, thenuv pip install -r <file>into a clean venv.requirements.lockdeploy/requirements.lockrequirements/prod.lockrequirements.pip,requirements(no extension),deploy/requirements.frozenSo #1265 fixes the shapes in this issue. The probe is still an extension allowlist, though, and
uv export -o/uv pip compile -oaccept any name. Exports such asrequirements.pip, an extensionlessrequirements, or*.frozenstill lose the wheel silently. I haven't filed those separately because the names are less common. A content-based probe (any regular file under the project that names.socket/vendor/pypi/<uuid>/, skipping the existingPROBE_SKIPPED_DIRS) would close the class, if that's wanted.Generated by Claude Code