[agent] Found by the scheduled uv bug-hunt routine (ledger #310).
Summary
In hosted mode on a native uv project, the redirect ledger stores the entire pre-edit pyproject.toml and the entire root [[package]] block of uv.lock (the one carrying [package.metadata] requires-dist) as revert fragments. Any later change to those spans that has nothing to do with the patch breaks the revert. That includes adding a description line, or uv add <anything>, which rewrites both the dependencies array and the root lock block. After that, socket-patch rollback and socket-patch remove fail with:
uv.lock: content matches neither the redirected nor the original fragment for redirect_uv_lock_wheel — the file drifted; re-run `scan --mode hosted` to normalize (pyproject.toml, uv.lock)
Following that advice doesn't help. The re-scan exits 0 and appends a new six-block edit, but it never refreshes the stale pyproject/root-block fragments, so the next rollback fails with the same error. There is no supported way left to remove the hosted patch. The project keeps the Socket [tool.uv.sources] url, and uv.lock keeps the url and the patched hash, until someone edits both files by hand.
Script locks behave the same way: after uv add --script s.py idna==3.7, rollback fails on s.py.lock.
Vendored mode handles the same uv add correctly. Rollback succeeds, uv sync --locked passes, and pristine bytes are installed.
Impact
uv add / uv remove are the everyday uv workflow, so in practice most hosted uv projects lose rollback and remove soon after they're patched. The error message sends users into a loop that can't succeed.
Repro
Needs a patch API that serves a hosted pypi patch. I used a local mock of the authenticated routes (/v0/orgs/<org>/patches/{batch,by-package,package,view} plus the wheel), modeled on crates/socket-patch-cli/tests/vex_e2e_common/uv.rs::ScanApi, serving a patched six-1.16.0 wheel.
SP="socket-patch --api-url http://127.0.0.1:18080 --api-token t --org test-org"
mkdir p && cd p
printf '[project]\nname = "uvp"\nversion = "0.1.0"\nrequires-python = ">=3.9"\ndependencies = ["six==1.16.0"]\n' > pyproject.toml
uv lock && uv sync
$SP scan --mode hosted --json --yes # redirected: 1 (pyproject.toml, uv.lock)
uv add idna==3.7 # or: add `description = "hello"` to [project]
$SP scan --mode hosted --json --yes # redirected: 1, rewrittenFiles [uv.lock], no warnings
$SP rollback --json # exit 1, status partial_failure, hosted.failed[0].error = "...the file drifted; re-run `scan --mode hosted` to normalize"
grep -c 127.0.0.1:18080 pyproject.toml uv.lock # 1 / 3: the patch is still wired
$SP remove pkg:pypi/[email protected] --json --yes # error hosted_revert_failed, same message
Control: the same flow without the intermediate edit rolls back cleanly (exit 0, no hosted refs left).
Expected vs actual
- Expected:
rollback restores the upstream registry source for the patched package and keeps the user's unrelated edits, as vendored mode already does for the same uv add. At minimum, the drift remedy it prints (re-run scan --mode hosted to normalize) should actually make the next rollback succeed. That is how Poetry and PDM behave: redirect_poetry_lock_package / redirect_pdm_lock_package are rebased on re-scan.
- Actual: rollback and remove refuse on every run, and the re-scan never repairs the ledger.
OS × uv matrix (main f6b7fb9)
| OS |
uv 0.4.30 |
uv 0.5.31 |
uv 0.8.17 |
uv 0.12.21 |
| Linux |
fail |
fail |
fail |
fail |
| macOS (arm64) |
– |
fail |
– |
fail |
| Windows |
– |
fail |
– |
fail |
Every cell covers both triggers (the pyproject description edit and uv add idna==3.7). The no-edit control passes in every cell. The script-lock variant (uv add --script) fails on Linux with 0.12.21.
First bad
The published 4.0.0 release predates the current uv rewriter (#238 / #239 landed after it) and writes a different shape, so it isn't comparable. The defect is in unreleased main.
Suspect code
crates/socket-patch-core/src/patch/redirect/mod.rs:4672: record_python_metadata_edit records the whole pyproject.toml (edit.original / edit.rewritten) as the fragment, not just the [tool.uv.sources] entry.
crates/socket-patch-core/src/patch/redirect/mod.rs:4536: record_python_lock_edits records whole [[package]] blocks, including the root project block that every uv add / uv remove rewrites.
crates/socket-patch-cli/src/commands/scan/hosted.rs:33: REBASE_KINDS has no redirect_uv_lock_wheel, and an already-redirected pyproject/root block produces no fresh edit, so a re-scan can never rebase the stale fragments.
crates/socket-patch-core/src/patch/redirect/replay.rs:745: the refusal that is hit.
Probe run
https://github.com/SocketDev/socket-patch/actions/runs/36774452940 (ubuntu / macos / windows × uv 0.5.31 and 0.12.21)
[agent] Found by the scheduled uv bug-hunt routine (ledger #310).
Summary
In hosted mode on a native uv project, the redirect ledger stores the entire pre-edit
pyproject.tomland the entire root[[package]]block ofuv.lock(the one carrying[package.metadata] requires-dist) as revert fragments. Any later change to those spans that has nothing to do with the patch breaks the revert. That includes adding adescriptionline, oruv add <anything>, which rewrites both the dependencies array and the root lock block. After that,socket-patch rollbackandsocket-patch removefail with:Following that advice doesn't help. The re-scan exits 0 and appends a new six-block edit, but it never refreshes the stale pyproject/root-block fragments, so the next rollback fails with the same error. There is no supported way left to remove the hosted patch. The project keeps the Socket
[tool.uv.sources]url, anduv.lockkeeps the url and the patched hash, until someone edits both files by hand.Script locks behave the same way: after
uv add --script s.py idna==3.7, rollback fails ons.py.lock.Vendored mode handles the same
uv addcorrectly. Rollback succeeds,uv sync --lockedpasses, and pristine bytes are installed.Impact
uv add/uv removeare the everyday uv workflow, so in practice most hosted uv projects lose rollback and remove soon after they're patched. The error message sends users into a loop that can't succeed.Repro
Needs a patch API that serves a hosted pypi patch. I used a local mock of the authenticated routes (
/v0/orgs/<org>/patches/{batch,by-package,package,view}plus the wheel), modeled oncrates/socket-patch-cli/tests/vex_e2e_common/uv.rs::ScanApi, serving a patchedsix-1.16.0wheel.Control: the same flow without the intermediate edit rolls back cleanly (exit 0, no hosted refs left).
Expected vs actual
rollbackrestores the upstream registry source for the patched package and keeps the user's unrelated edits, as vendored mode already does for the sameuv add. At minimum, the drift remedy it prints (re-run scan --mode hosted to normalize) should actually make the next rollback succeed. That is how Poetry and PDM behave:redirect_poetry_lock_package/redirect_pdm_lock_packageare rebased on re-scan.OS × uv matrix (main
f6b7fb9)Every cell covers both triggers (the pyproject
descriptionedit anduv add idna==3.7). The no-edit control passes in every cell. The script-lock variant (uv add --script) fails on Linux with 0.12.21.First bad
The published 4.0.0 release predates the current uv rewriter (#238 / #239 landed after it) and writes a different shape, so it isn't comparable. The defect is in unreleased main.
Suspect code
crates/socket-patch-core/src/patch/redirect/mod.rs:4672:record_python_metadata_editrecords the wholepyproject.toml(edit.original/edit.rewritten) as the fragment, not just the[tool.uv.sources]entry.crates/socket-patch-core/src/patch/redirect/mod.rs:4536:record_python_lock_editsrecords whole[[package]]blocks, including the root project block that everyuv add/uv removerewrites.crates/socket-patch-cli/src/commands/scan/hosted.rs:33:REBASE_KINDShas noredirect_uv_lock_wheel, and an already-redirected pyproject/root block produces no fresh edit, so a re-scan can never rebase the stale fragments.crates/socket-patch-core/src/patch/redirect/replay.rs:745: the refusal that is hit.Probe run
https://github.com/SocketDev/socket-patch/actions/runs/36774452940 (ubuntu / macos / windows × uv 0.5.31 and 0.12.21)