Skip to content

Hosted uv rollback and remove are stuck after any unrelated pyproject.toml edit or uv add, and the suggested re-scan doesn't fix it #379

Description

[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)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions