Skip to content

uv projects never pick up a superseding patch: hosted re-scan lists the upgrade in updates[] but refuses its own earlier [tool.uv.sources] pin (exit 0, still on the old uuid), and vendored re-scan fails pypi_uv_source_already_exists #742

Description

[agent] Found by the scheduled uv bug-hunt routine (ledger #310).

Summary

When the patch API publishes a new patch uuid for a package that a uv project already has wired, re-running scan reports the upgrade but never applies it:

  • Hosted (scan --mode hosted), on a uv project (pyproject.toml + uv.lock) and on a PEP 723 script lock: status: success, exit 0, updates: [{oldUuid: A, newUuid: B}], rollout.counts.upgrade: 1, but redirect.redirected: 0 and rewrittenFiles: []. The only signal is the warning redirect_uv_project_unsupported (or redirect_uv_script_unsupported): "pyproject.toml: Python project already declares a source for six; revert it before applying a different patch". The source it names is socket-patch's own hosted pin from the previous scan. pyproject.toml and uv.lock stay on uuid A. --dry-run previews the same thing.
  • Vendored (scan --mode vendored), project and script lock: exit 1, partial_failure, pypi_uv_source_already_exists ("[tool.uv.sources] already routes six to a socket-patch vendored wheel; run socket-patch vendor --revert before re-vendoring"), or pypi_lock_source_already_exists for a script. .socket/vendor/pypi/<uuid A>/ stays wired.

On the same mock, a hosted requirements.txt project re-pins to uuid B correctly (rewrittenFiles: ["requirements.txt"]), so the hosted refusal is specific to the uv [tool.uv.sources] writer.

Impact

  • A superseding patch (for example a fix for a broken patch, or one that covers more advisories) never reaches uv users in hosted mode. CI stays green: exit 0, success, and updates[] even claims the upgrade happened. The project keeps installing the old patched wheel, and if that artifact is ever withdrawn, every fresh uv sync 404s while scan keeps reporting success.
  • Vendored mode fails loudly, but it tells the user to revert a wiring that socket-patch itself wrote, which contradicts the contract below.
  • Workaround (verified): socket-patch rollback, then scan --mode hosted again. That re-pins to uuid B, and uv sync --locked installs the new patched bytes.

Repro (Linux, real uv, local mock patch API)

The mock answers POST /v0/orgs/acme/patches/{batch,package}, GET …/by-package/… and GET …/view/<uuid>, plus SOCKET_PYPI_JSON_API. It serves a deterministic patched six-1.16.0-py2.py3-none-any.whl under uuid A (…0001, SOCKET_PATCHED = 1). It's then switched to offer only uuid B (…0004, a different patched six.py, SOCKET_PATCHED = 2) for pkg:pypi/six@1.16.0, and the old artifact URL stays served.

cat > pyproject.toml <<'EOF'
[project]
name = "app"
version = "0.1.0"
requires-python = ">=3.9"
dependencies = ["six==1.16.0", "idna==3.7"]
EOF
uv lock && uv sync -p 3.11
SP="--api-url $M --api-token fake --org acme --patch-server-url $M"
socket-patch scan --mode hosted --json --yes $SP     # wires uuid A: success, redirected 1
# patch API now offers only uuid B for pkg:pypi/six@1.16.0
socket-patch scan --mode hosted --json --yes $SP
#  exit 0, status "success"
#  updates: [{"purl":"pkg:pypi/six@1.16.0","oldUuid":"…0001","newUuid":"…0004"}]
#  rollout.counts: {"new":0,"deferred":0,"upgrade":1,"already":0}
#  redirect: {"redirected":0,"rewrittenFiles":[],"warnings":[{"code":"redirect_uv_project_unsupported",
#             "detail":"pyproject.toml: Python project already declares a source for six; revert it before applying a different patch"}]}
grep -o 'aaaaaaaa-0000-4000-8000-00000000000[0-9]' pyproject.toml uv.lock   # still …0001 only
# vendored:
socket-patch scan --mode vendored --json --yes $SP --vendor-url $M   # uuid A vendored
# switch to uuid B
socket-patch scan --mode vendored --json --yes $SP --vendor-url $M
#  exit 1, partial_failure, failed: pypi_uv_source_already_exists

Expected vs actual

  • Expected (crates/socket-patch-cli/CLI_CONTRACT.md, "Per-run limit on new patches", "Classification"): an UPGRADE row ("the selection supersedes the recorded uuid … or the recorded uuid is no longer offered") hands the writer "the selected uuid", and "updates[] lists exactly the UPGRADE rows the run acts on". For vendored mode, the scan --vendor paragraph says: "A package the ledger holds at an older patch uuid is still re-vendored automatically when discovery selects the newer patch (its old uuid dir is removed — vendor_stale_artifact_removed)".
  • Actual: hosted lists the upgrade in updates[], then refuses it with a warning and exits 0 on the old uuid. Vendored refuses with exit 1.

Matrix (Linux)

uv hosted project hosted script lock vendored project vendored script lock
0.5.31 fail (exit 0, not re-pinned) – – –
0.8.17 fail (×2, also --dry-run) fail fail (exit 1) fail (exit 1, pypi_lock_source_already_exists)
0.12.23 (latest) fail – fail (exit 1) –
control: hosted requirements.txt (uv pip) pass: re-pinned to uuid B

Main 045d7ec (CLI 4.0.0). The behaviour comes from the writer, not from uv, so it doesn't depend on the OS. Not bisected: the uuid check has been in same_hosted_artifact since it was added in #358.

Suspect code

Note: the vendored re-vendor also fails on a plain requirements.txt project (pypi_requirements_already_vendored, exit 1). That's outside uv, so it's been handed to the pip routine rather than folded into this issue.

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions