You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[agent] Found by the scheduled uv bug-hunt routine (ledger #310).
Summary
The patched package is declared in a dependency group: PEP 735 [dependency-groups] dev = ["six==1.16.0", "attrs>=20"], or the legacy [tool.uv] dev-dependencies. scan --mode vendored rewrites the root package's [package.metadata.requires-dev] line in uv.lock. That fragment covers the wholedev = [ … ] line, with every element (pypi_uv.rs:1224, :1333-1343), not just six's element.
Any later edit to that same group by the user rewrites the whole line, even though six's own element is unchanged. Examples: uv add --dev zipp, uv add --dev idna==3.7, uv remove --dev attrs. After that edit:
vendor --revert (also remove / rollback) treats the uv.lock record as drift (vendor_lock_entry_drifted, vendor_artifact_kept, vendor_revert_kept). uv.lock keeps { name = "six", path = ".socket/vendor/…" } in requires-dev and the [[package]] six path source.
The pyproject.toml record still matches, so it is reverted: [tool.uv.sources] six = { path = … } is deleted (pypi_uv.rs:968-990).
uv sync --locked / uv lock --locked then fail: "The lockfile at uv.lock needs to be updated, but --locked was provided". vendor --revert reports status: "success" and exits 0. remove and rollback exit 1 with partialFailure, after pyproject.toml is already written.
A vendored → hosted takeover (scan --mode hosted) on the same shape is worse. It warns redirect_vendored_revert_failed: "part of its vendored wiring was edited since vendoring, so it is left in place; NOT switched to hosted". But it has already removed six's vendored source from pyproject.toml, so nothing was "left in place". It exits 0 with status: "success", and uv sync --locked fails.
Edits that don't touch the group are fine. uv add idna, uv add --optional x idna and uv add --group lint idna all revert cleanly, as do the same edits when six is in dependencies or an extra (each requires-dist element is its own fragment). Hosted rollback / remove on the same shape after uv add --dev also pass.
This has the same missing pair gate as #806 (a pyproject record reverted while its uv.lock partner is drift-kept). The trigger is different, though, and much more common. #806 needs a user-authored override-dependencies plus a transitive override, and its fragment is [manifest] overrides. Here the user only has to run uv add --dev <anything> on a project whose patched package is a dev dependency. Making the [manifest] overrides match semantic for #806 wouldn't fix this one.
Impact
The everyday uv add --dev pytest after vendoring makes every later unwind (vendor --revert, remove, rollback, a hosted takeover) leave a project that uv sync --locked rejects, so CI goes red. vendor --revert and the takeover report success.
The ledger entry and .socket/vendor/pypi/<uuid> are kept forever. The suggested remedy ("undo the drift … and re-run vendor --revert") can't be followed: the user never edited six's entry, and uv will keep writing the group line its own way.
A plain uv sync silently relocks to the unpatched registry six.
Repro
The mock serves POST /patch/package, the patched six 1.16.0 wheel with SRI sha512, and a /pypi pass-through; it's the same harness as #806 / #788. attrs / zipp / idna resolve from PyPI.
mkdir app &&cd app
cat > pyproject.toml <<'EOF'[project]name = "app"version = "0.1.0"requires-python = ">=3.9"dependencies = [][dependency-groups]dev = ["six==1.16.0", "attrs>=20"]EOF
uv lock && uv sync
SP="socket-patch --api-url $MOCK --api-token fake --org acme --patch-server-url $MOCK --vendor-url $MOCK"$SP scan --mode vendored --yes # uv.lock requires-dev: dev = [{ name = "attrs", … }, { name = "six", path = ".socket/vendor/…" }]
uv sync --locked # ok, six patched
uv add --dev zipp # uv rewrites the whole `dev = [...]` requires-dev line (six's element unchanged)$SP vendor --revert --yes --json # exit 0, status "success"; events vendor_lock_entry_drifted / vendor_artifact_kept / vendor_revert_kept
grep -c socket/vendor uv.lock # 1 (requires-dev six still a path source; [[package]] six still path)
grep -c socket/vendor pyproject.toml # 0 ([tool.uv.sources] six already removed)
uv sync --locked # error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided.# Takeover variant: from the state after `uv add --dev zipp`$SP scan --mode hosted --yes --json # exit 0, status "success", warning redirect_vendored_revert_failed "… left in place …"
grep -c socket/vendor pyproject.toml # 0, although the warning says the wiring was left in place
uv sync --locked # same error
Expected vs actual
Expected: docs/testing/uv-compatibility.md:176-178 says: "Script and lock edits are treated as a pair: conflicting changes preserve both files and their recovery state rather than restoring only one side." crates/socket-patch-cli/CLI_CONTRACT.md:714 says drifted fragments "are left alone with a vendor_lock_entry_drifted warning; the drift-kept artifact and entry stay". CLI_CONTRACT.md:164 says, for a takeover: "A takeover revert that leaves vendored wiring in place is refused with redirect_vendored_revert_failed … The ledger entry and artifact are kept, and the package stays vendored and skipped." So either the revert recognizes six's element inside a re-serialized group line (it is byte-identical) and unwinds both files, or it keeps both files wired so uv sync --locked keeps passing. PEP 723 script locks already do the latter: after a relock, both the script and the .py.lock are drift-kept together and uv lock --script --locked passes.
Actual: pyproject.toml is reverted and uv.lock is kept, so uv sync --locked fails. vendor --revert and the hosted takeover exit 0 with status: "success", and the takeover's warning misreports the project state.
Matrix
On main 045d7ec. "fail" means uvlock_refs=1 pyproject_refs=0 and uv sync --locked fails. Every Linux cell ran at least twice.
OS
uv
group spelling
edit
vendor --revert
remove
rollback
hosted takeover
Linux
0.2.37
legacy dev-dependencies
uv add --dev zipp
fail
–
–
–
Linux
0.4.30
PEP 735 and legacy
uv add --dev zipp
fail
–
–
–
Linux
0.5.31
PEP 735 and legacy
uv add --dev idna==3.7
fail (exit 0)
fail (exit 1)
fail (exit 1)
–
Linux, macOS, Windows (probe)
0.8.17 / 0.12.23
PEP 735 and legacy
uv add --dev idna==3.7
fail (exit 0)
fail (exit 1)
fail (exit 1)
–
Linux
0.12.23
PEP 735
uv add --dev zipp, uv remove --dev attrs
fail
–
–
–
Linux
0.8.17 / 0.12.23
PEP 735
uv add --dev zipp
–
–
–
fail (exit 0, "left in place")
Linux, macOS, Windows (probe); Linux 0.5.31
0.8.17 / 0.12.23
PEP 735 and legacy
uv add --dev idna==3.7, hosted scan then remove / rollback
drift-kept, but both files kept, so --locked passes
–
–
–
First bad version: not bisected. The published v4.0.0 can't vendor against the harness (no_local_source, the pre-v5 vendor flow).
Suspect code
crates/socket-patch-core/src/vendor/pypi_uv.rs:1333-1343 (rewrite_root_metadata_entries): the uv_lock_requires_dev record's old / new text is the whole <group> = [ … ] line, so any sibling added to or removed from that group invalidates it. The comment at :1224 says this is deliberate, to tell apart identically pinned groups. Keying on the group name plus the element would do that without capturing the siblings.
crates/socket-patch-core/src/vendor/pypi_uv.rs:829-848: the uv_lock_requires_dev arm only matches the exact recorded line (replace_fragment), and the "already converged" check looks for the old line verbatim.
crates/socket-patch-cli/src/commands/scan/hosted.rs:1795-1933: the takeover emits redirect_vendored_revert_failed ("left in place") after revert_uv has already written pyproject.toml.
[agent] Found by the scheduled uv bug-hunt routine (ledger #310).
Summary
The patched package is declared in a dependency group: PEP 735
[dependency-groups] dev = ["six==1.16.0", "attrs>=20"], or the legacy[tool.uv] dev-dependencies.scan --mode vendoredrewrites the root package's[package.metadata.requires-dev]line in uv.lock. That fragment covers the wholedev = [ … ]line, with every element (pypi_uv.rs:1224,:1333-1343), not just six's element.Any later edit to that same group by the user rewrites the whole line, even though six's own element is unchanged. Examples:
uv add --dev zipp,uv add --dev idna==3.7,uv remove --dev attrs. After that edit:vendor --revert(alsoremove/rollback) treats the uv.lock record as drift (vendor_lock_entry_drifted,vendor_artifact_kept,vendor_revert_kept). uv.lock keeps{ name = "six", path = ".socket/vendor/…" }in requires-dev and the[[package]] sixpath source.[tool.uv.sources] six = { path = … }is deleted (pypi_uv.rs:968-990).uv sync --locked/uv lock --lockedthen fail: "The lockfile atuv.lockneeds to be updated, but--lockedwas provided".vendor --revertreportsstatus: "success"and exits 0.removeandrollbackexit 1 withpartialFailure, after pyproject.toml is already written.A vendored → hosted takeover (
scan --mode hosted) on the same shape is worse. It warnsredirect_vendored_revert_failed: "part of its vendored wiring was edited since vendoring, so it is left in place; NOT switched to hosted". But it has already removed six's vendored source from pyproject.toml, so nothing was "left in place". It exits 0 withstatus: "success", anduv sync --lockedfails.Edits that don't touch the group are fine.
uv add idna,uv add --optional x idnaanduv add --group lint idnaall revert cleanly, as do the same edits when six is independenciesor an extra (eachrequires-distelement is its own fragment). Hosted rollback / remove on the same shape afteruv add --devalso pass.This has the same missing pair gate as #806 (a pyproject record reverted while its uv.lock partner is drift-kept). The trigger is different, though, and much more common. #806 needs a user-authored
override-dependenciesplus a transitive override, and its fragment is[manifest] overrides. Here the user only has to runuv add --dev <anything>on a project whose patched package is a dev dependency. Making the[manifest] overridesmatch semantic for #806 wouldn't fix this one.Impact
uv add --dev pytestafter vendoring makes every later unwind (vendor --revert,remove,rollback, a hosted takeover) leave a project thatuv sync --lockedrejects, so CI goes red.vendor --revertand the takeover report success..socket/vendor/pypi/<uuid>are kept forever. The suggested remedy ("undo the drift … and re-runvendor --revert") can't be followed: the user never edited six's entry, and uv will keep writing the group line its own way.uv syncsilently relocks to the unpatched registry six.Repro
The mock serves
POST /patch/package, the patched six 1.16.0 wheel with SRI sha512, and a/pypipass-through; it's the same harness as #806 / #788. attrs / zipp / idna resolve from PyPI.Expected vs actual
vendor_lock_entry_driftedwarning; the drift-kept artifact and entry stay". CLI_CONTRACT.md:164 says, for a takeover: "A takeover revert that leaves vendored wiring in place is refused withredirect_vendored_revert_failed… The ledger entry and artifact are kept, and the package stays vendored and skipped." So either the revert recognizes six's element inside a re-serialized group line (it is byte-identical) and unwinds both files, or it keeps both files wired souv sync --lockedkeeps passing. PEP 723 script locks already do the latter: after a relock, both the script and the.py.lockare drift-kept together anduv lock --script --lockedpasses.uv sync --lockedfails.vendor --revertand the hosted takeover exit 0 withstatus: "success", and the takeover's warning misreports the project state.Matrix
On main
045d7ec. "fail" meansuvlock_refs=1 pyproject_refs=0anduv sync --lockedfails. Every Linux cell ran at least twice.vendor --revertremoverollbackdev-dependenciesuv add --dev zippuv add --dev zippuv add --dev idna==3.7uv add --dev idna==3.7uv add --dev zipp,uv remove --dev attrsuv add --dev zippuv add --dev idna==3.7, hosted scan thenremove/rollbackdependenciesor in an extrauv add --dev,uv add --optional,uv add,uv removedevuv add idna,uv add --optional x idna,uv add --group lint idna(another group)override-dependenciesuv add --script,uv lock --script --upgrade-package--lockedpassesFirst bad version: not bisected. The published v4.0.0 can't vendor against the harness (
no_local_source, the pre-v5 vendor flow).Suspect code
crates/socket-patch-core/src/vendor/pypi_uv.rs:1333-1343(rewrite_root_metadata_entries): theuv_lock_requires_devrecord's old / new text is the whole<group> = [ … ]line, so any sibling added to or removed from that group invalidates it. The comment at:1224says this is deliberate, to tell apart identically pinned groups. Keying on the group name plus the element would do that without capturing the siblings.crates/socket-patch-core/src/vendor/pypi_uv.rs:829-848: theuv_lock_requires_devarm only matches the exact recorded line (replace_fragment), and the "already converged" check looks for the old line verbatim.crates/socket-patch-core/src/vendor/pypi_uv.rs:968-990: pyproject.toml is written even when a uv.lock record was drift-kept. The unit testrevert_warns_and_skips_on_drifted_lock_fragment(:2612-2650) asserts this half-revert ("The pyproject side (undrifted) was still reverted"). That conflicts with the pair rule in uv-compatibility.md:176-178 and is the same gap 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 hits.crates/socket-patch-cli/src/commands/scan/hosted.rs:1795-1933: the takeover emitsredirect_vendored_revert_failed("left in place") afterrevert_uvhas already written pyproject.toml.Probe runs
bughunt/uv/20261005-dev-group-drift, ubuntu / macos / windows × uv 0.8.17 / 0.12.23)Related: #806 (same missing pair gate,
[manifest] overridesfragment).