Repository navigation
Vendored uv transitive package later added as a direct dependency (uv add six==1.16.0): vendor --revert / remove / rollback half-revert the pair, so uv sync --locked fails and vendor --check claims nothing references the wheel #1374
Description
[agent] Found by the scheduled uv bug-hunt routine (ledger #310).
Summary
Vendor a transitive package in a uv project (here six, pulled in by python-dateutil and held at 1.16.0 by a user constraint-dependencies). Vendored mode wires it through [tool.uv] override-dependencies + [tool.uv.sources] and records five wiring entries: uv_override, uv_sources_entry, uv_lock_package, uv_lock_manifest_overrides and uv_lock_manifest_constraints.
Then the user promotes it to a direct dependency, for example to pin it: uv add six==1.16.0. uv writes a new root requires-dist element, { name = "six", path = ".socket/vendor/pypi/<uuid>/six-1.16.0-…whl" }, because the socket-written source routes it. socket-patch never recorded that element.
vendor --revert, remove six and rollback then revert every recorded fragment. They drop the sources line and the override, and put the lock's [[package]] and [manifest] back on PyPI. They leave the root requires-dist element still pointing at the wheel. Only after writing does the residual-reference guard notice that uv.lock still names the uuid dir, and it keeps the wheel and the ledger entry.
The result is a pair uv rejects:
uv sync --lockedfails with "The lockfile atuv.lockneeds to be updated".vendor --revertexits 0.removeandrollbackexit 1.vendor --checkthen exits 1 withwiring missing: no lockfile or config references .socket/vendor/pypi/<uuid> any more … re-run socket-patch vendor to rewire it. That's false, becauseuv.lockstill references it, and the remedy re-vendors the package the user just asked to unwind.
Impact
A user who pins a vendored transitive dependency directly, which is a normal uv add, can't unwind it cleanly. Every --locked / --frozen-checked CI install breaks after the revert. The only recovery is to know to run uv lock by hand and then vendor --revert again (that converges).
The same uv add after uv remove python-dateutil (the #1287 shape plus a direct re-add) fails the same way, on main and on PR #1337.
Repro
Requires a patch API serving a six@1.16.0 patch. I used the routine's local mock with --api-url/--patch-server-url/--vendor-url http://127.0.0.1:8765, abbreviated sp below.
mkdir demo && cd demo
cat > pyproject.toml <<'EOF'
[project]
name = "demo"
version = "0.1.0"
requires-python = ">=3.9"
dependencies = ["attrs>=20"]
[tool.uv]
constraint-dependencies = ["six==1.16.0"]
EOF
uv add -q python-dateutil==2.9.0.post0 && uv sync -q
sp scan --mode vendored --yes # six wired via override + sources, exit 0
uv add -q six==1.16.0 # promote to direct; uv.lock root requires-dist now has { name = "six", path = ".socket/vendor/pypi/<uuid>/…" }
uv sync -q --locked # ok, six patched
sp vendor --check; echo $? # 0
sp vendor --revert --json; echo $? # 0: vendor_revert_residual_reference + vendor_artifact_kept + vendor_revert_kept
uv sync -q --locked # error: The lockfile at `uv.lock` needs to be updated
sp vendor --check; echo $? # 1: "wiring missing: no lockfile or config references … re-run socket-patch vendor"
grep -n aaaaaaaa uv.lock # requires-dist = [ …, { name = "six", path = ".socket/vendor/pypi/…/six-1.16.0-py2.py3-none-any.whl" } ]After the revert, pyproject.toml has no sources or override, and [[package]] six is back on registry = "https://pypi.org/simple". The root [package.metadata] requires-dist still carries the path element.
Expected vs actual
- Expected: the revert leaves a pair
uv sync --lockedaccepts. Either restore the root element to the specifierpyproject.tomldeclares now ({ name = "six", specifier = "==1.16.0" }, the Vendored uv revert writes the pre-vendor specifier back into uv.lock after the user changes the vendored package's version spec, souv sync --lockedfails (vendor --revert / remove / rollback exit 0) #840 respell rule for root requirement entries), or, failing that, write nothing and drift-keep both files (fail closed, as the PEP 723 script lane already does for this exact shape). CLI_CONTRACT's revert contract keeps the artifact when a file still references it. It doesn't allow a half-written uv pair, and Vendored uv with a user-authoredoverride-dependencies: after any relock (uv add,uv lock --upgrade-package),vendor --revert/removerevert pyproject.toml but keep the vendored[manifest] overridesentry in uv.lock, souv sync --lockedfails (vendor --revert exits 0) #806 / Vendored uv package in a dependency group: afteruv add --dev/uv remove --dev,vendor --revert,remove,rollbackand the hosted takeover revert pyproject.toml but keep uv.lock's vendored requires-dev entry, souv sync --lockedfails (exit 0, "success") #821 / Vendored uv revert writes the pre-vendor specifier back into uv.lock after the user changes the vendored package's version spec, souv sync --lockedfails (vendor --revert / remove / rollback exit 0) #840 were the same class of half-revert. vendor --checkshould not claim "no lockfile or config references" the uuid dir whileuv.lockdoes.- Actual: pyproject and four of the five lock fragments are reverted, the root
requires-distpath element is left,uv sync --lockedfails, andvendor --revertexits 0.
Matrix (Linux, main 85105c9)
| unwind | uv 0.5.31 | uv 0.12.24 |
|---|---|---|
vendor --revert (parent kept, uv add six==1.16.0) |
fail: exit 0, --locked fails, check 1 |
fail (same) |
remove six --yes |
fail: exit 1, --locked fails |
fail |
rollback --yes |
fail: exit 1, --locked fails |
fail |
vendor --revert after uv remove python-dateutil + uv add six==1.16.0 |
fail (main and PR #1337) | fail (main and PR #1337) |
hosted takeover (scan --mode hosted) |
pass: redirect_vendored_revert_failed, files untouched |
pass |
PEP 723 script, uv add --script s.py six==1.16.0, then revert |
fail-closed: drift-keep, both files untouched, still patched | same |
recovery: uv lock, then vendor --revert |
pass, converges, check 0 | pass |
The checks are text-level uuid checks with no OS-specific branch, so I didn't run a probe.
Suspect code
crates/socket-patch-core/src/vendor/pypi_uv.rs:795(revert_uv): only the recorded records are reverted. The transitive wiring has nouv_lock_requires_distrecord, so a root element the user'suv addcreated through the socket-written source is never restored, and nothing checks the reverted pair for leftover uuid references before writing.crates/socket-patch-core/src/vendor/pypi.rs:2416(residual-reference guard): it runs after the flavor revert has already written, so it can only keep the artifact, not prevent the inconsistent write.- The
vendor --check"wiring missing" verdict ignores arequires-distpath element in uv.lock.
Activity
mikolalysenko commentedon Oct 9, 2026
[agent] Triaged as p1 (uv). Possibly related to #1287 / #1337, which fix the vendored uv revert path for a transitive package whose lock fragment changed shape. This report covers a different transition (transitive → direct after
uv add), and I haven't confirmed that #1337's change covers it, so this stays a separate issue for now.Generated by Claude Code
mikolalysenko commentedon Oct 11, 2026
[agent] Claiming this issue (shared root cause: vendored uv revert only restores recorded fragments and writes the pair without checking uv.lock still routes through the wheel). Branch: agent/fix-uv-revert-direct-requires-dist. Claim-ID: 2026-10-11T12:21:26Z-93f09a
Generated by Claude Code
mikolalysenko commentedon Oct 11, 2026
[agent] Draft fix PR: #1384
Generated by Claude Code
Fix uv revert after a vendored dep goes direct