Repository navigation
uv vendored → hosted takeover strands a package that vendored mode pinned to a different version than uv.lock: the wet run reverts to the unpatched release (exit 1), while --dry-run previews a clean takeover #723
Description
[agent] Found by the scheduled uv bug-hunt routine (ledger #310).
Summary
On a uv project, vendored mode pins the patched package to the patch's version even when uv.lock resolved another one (pypi_uv.rs does this on purpose: the unit test override_wiring_matches_fixture_byte_identically covers "the lock's 1.17.0 → 1.16.0 version pin-down"). For example, python-dateutil pulls in six 1.17.0, the manifest patch is for six@1.16.0, and vendor writes override-dependencies = ["six==1.16.0"] plus a path source, and rewrites the lock entry to version = "1.16.0". A direct six>=1.15 gets the same treatment, with no warning.
The vendored → hosted takeover (scan --mode hosted over that project, from #503) doesn't account for this:
- Wet run: it reverts the vendored wiring first. That deletes the committed wheel and the ledger entry, and restores
six 1.17.0inuv.lock. Only then does it ask the uv rewriter to pinsix@1.16.0, which no longer has a lock entry:redirect_uv_entry_not_found,redirected: 0,partial_failure,redirect_takeover_unpatched, exit 1. A project that installed patched six before the command now installs unpatchedsix 1.17.0from PyPI. --dry-run: reportsstatus: success,redirected: 1and onlyredirect_would_revert_vendored. Every previewed takeover is counted as confirmed (hosted.rs:1018,confirmed.extend(dry_run_takeover)), so the strand is never predicted.
This is the uv counterpart of #699 (requirements.txt). The cause is different: the lock entry the hosted rewriter needs disappears with the revert. Open PR #708 adds a takeover preflight only for requirements wiring (preflight_requirements_takeover), and I verified this case still strands on its head 5f291a5.
Impact
- A user who previews with
--dry-run, sees a clean takeover, and runs it loses the patch.uv sync --lockedsucceeds and installs the vulnerable upstream release, with the vendored wheel already deleted. - It's reachable whenever vendored mode was run with a manifest entry whose version differs from the lock's. For example, a committed
.socket/manifest.jsonfrom agent mode orgetrecordssix@1.16.0, anduv lock --upgradelater moved the transitivesixto 1.17.0. Hosted discovery then findssix@1.16.0in the vendored lock (Socket's own references stay discoverable), so the API offers the 1.16.0 patch and the takeover runs. - The printed remedy ("re-run
scan --mode hosted") can't work: hosted mode never pins a version the lock doesn't resolve. Onlyscan --mode vendoredrecovers.
Repro
I used the mode_migration_pypi.rs harness: stage_manifest (six@1.16.0), the prebuilt fixture server for vendor, and mount_hosted_api for the hosted scan. Real uv was used for lock and install.
mkdir proj && cd proj
cat > pyproject.toml <<'EOF'
[project]
name = "demo"
version = "0.1.0"
requires-python = ">=3.9"
dependencies = ["python-dateutil==2.9.0.post0"]
EOF
uv lock # six 1.17.0 (transitive)
# stage .socket/manifest.json for pkg:pypi/six@1.16.0, then:
socket-patch vendor --json # success, no warning; uv.lock six -> version "1.16.0", path source;
# pyproject gains override-dependencies = ["six==1.16.0"] + [tool.uv.sources]
uv sync --locked && .venv/bin/python -c 'import six; print(six.SOCKET_PATCHED)' # 1
socket-patch scan --mode hosted --yes --dry-run --json --api-url $API --org test-org --api-token x --patch-server-url $API
# status: success, redirect.redirected: 1, warnings: [redirect_would_revert_vendored] exit 0
socket-patch scan --mode hosted --yes --json --api-url $API --org test-org --api-token x --patch-server-url $API ; echo $?
# status: partial_failure, redirected: 0,
# warnings: redirect_takeover_reverted_vendored, redirect_uv_entry_not_found, redirect_takeover_unpatched ; exit 1
git status --short # .socket/vendor/** deleted, pyproject.toml + uv.lock back to six 1.17.0 from PyPI
rm -rf .venv && uv sync --locked && .venv/bin/python -c 'import six; print(getattr(six,"SOCKET_PATCHED","UNPATCHED"))' # UNPATCHEDThe direct variant dependencies = ["six>=1.15"] (lock: 1.17.0) behaves identically.
Expected vs actual
- Expected: CLI_CONTRACT §
scan --mode hosted/ thevendored_takeoverdoc inhosted.rs: "A takeover must leave the project FULLY hosted … A purl whose vendored state cannot be cleanly reverted … is REFUSED — skipped with an actionable error — never half-migrated". The dry-run preview must report the wet run's outcome (dry_run_predicts_drifted_takeover_refusalpins this for drifted wiring). When the reverted lock won't resolve the patch's version, the takeover should be refused before the revert, keeping the vendored patch, and the dry run should predict that refusal. A uv-side twin of the npmtakeover_refusalgates athosted.rs:1725, which today returnsNonefor every non-npm purl, would cover it. - Actual: the wet run reverts first and strands the package unpatched (exit 1). The dry run previews success with
redirected: 1.
Matrix (Linux, main 045d7ec)
| uv | transitive pin-down (dateutil → six 1.17.0) | direct six>=1.15 |
dry run | uv sync --locked after the wet run |
|---|---|---|---|---|
| 0.5.31 | strands (exit 1) | – | success, redirected: 1 |
unpatched 1.17.0 |
| 0.8.17 | strands (reproduced 2×) | strands | success, redirected: 1 |
unpatched 1.17.0 |
| 0.12.23 (newest) | strands | – | success, redirected: 1 |
unpatched 1.17.0 |
0.8.17 on PR #708 5f291a5 |
strands | – | success, redirected: 1 |
unpatched |
| control: lock already resolves 1.16.0 | pass (redirected: 1, patched) |
patched |
The decision is made on lock text, so it doesn't depend on the OS, and I ran no probe. First bad: #503 (0ac5b91a), which enabled the PyPI takeover. Before it the takeover was refused (#328), and the vendored patch survived.
Suspect code
crates/socket-patch-cli/src/commands/scan/hosted.rs:1018:confirmed.extend(dry_run_takeover).crates/socket-patch-cli/src/commands/scan/hosted.rs:1725:takeover_refusalgates npm only. PyPI has no "will the hosted rewriter reach this after the revert" check, and Fix PyPI vendored→hosted takeover stranding unreachable pins (#699) #708 adds one for requirements only.crates/socket-patch-core/src/vendor/pypi_uv.rs:1150: the lock entry'sversionis rewritten to the patch version (the pin-down that the revert undoes).crates/socket-patch-core/src/patch/redirect/mod.rs:4643:redirect_uv_entry_not_found, which the takeover runs into after reverting.
Related: #699 / PR #708 (requirements.txt shape), #568 (drift-kept ledger), #369 (the yarn berry reverse direction, closed).
Activity
uv: run 13 ledger (#723, #612 comment)
mikolalysenko commentedon Oct 3, 2026
[agent] Triaged as
priority:p1(uv). Not a duplicate of #699: that one is the requirements.txt rewriter only reaching the root file, and open PR #708 adds a gate just for that flavor (preflight_requirements_takeoverinsidetakeover_refusalinscan/hosted.rs). Here the cause is uv-specific. Vendored uv pins the lock entry to the patch version, so reverting it brings back the lock's own version, and the hosted uv rewriter can no longer find an entry for the patched version. The fix belongs in the sametakeover_refusalgate as a uv preflight (refuse when the restored lock won't resolve the patch version), and the dry-run preview has to predict that refusal too. Kept as its own fix; it will touch the same closure as #708.Generated by Claude Code
mikolalysenko commentedon Oct 5, 2026
[agent] Re-checked on main
9c43dfc(uv bug-hunt run 21, ledger #310). This still reproduces, the same way on both shapes and on uv 0.8.17 and 0.12.23 (4/4 cells).Setup:
uv lockresolves six 1.17.0, either directly (six>=1.15) or transitively (python-dateutil==2.9.0.post0). Thenget aaaaaaaa-…-001 --id --save-only --mode agentstagessix@1.16.0in the manifest, andvendorpins uv.lock to 1.16.0.uv sync --lockedthen installs patched six.None of the commits since
99f61d2(#858, #802, #807 and the others) touch the takeover gate.Generated by Claude Code
uv bug-hunt run 21: #890, #891; #723 / #767 still fail
mikolalysenko commentedon Oct 6, 2026
[agent] Claiming this issue (with #945; shared root cause: the PyPI vendored → hosted takeover gate
takeover_refusalinscan/hosted.rsonly checks requirements.txt reach, never whether the uv / Poetry hosted rewriter can pin the package after the revert). Branch: agent/fix-pypi-hosted-takeover-preflight. Claim-ID: 2026-10-06T15:20:47Z-fce7c7Generated by Claude Code
mikolalysenko commentedon Oct 6, 2026
[agent] Draft PR: #946
Generated by Claude Code
Test PyPI hosted takeovers that strand a package
Keep uv/Poetry packages vendored on bad takeover
Merge origin/main into arch-fix/takeover-atomic