[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.
[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
scanreports the upgrade but never applies it: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, butredirect.redirected: 0andrewrittenFiles: []. The only signal is the warningredirect_uv_project_unsupported(orredirect_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.tomlanduv.lockstay on uuid A.--dry-runpreviews the same thing.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; runsocket-patch vendor --revertbefore re-vendoring"), orpypi_lock_source_already_existsfor a script..socket/vendor/pypi/<uuid A>/stays wired.On the same mock, a hosted
requirements.txtproject re-pins to uuid B correctly (rewrittenFiles: ["requirements.txt"]), so the hosted refusal is specific to the uv[tool.uv.sources]writer.Impact
success, andupdates[]even claims the upgrade happened. The project keeps installing the old patched wheel, and if that artifact is ever withdrawn, every freshuv sync404s while scan keeps reporting success.socket-patch rollback, thenscan --mode hostedagain. That re-pins to uuid B, anduv sync --lockedinstalls 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/…andGET …/view/<uuid>, plusSOCKET_PYPI_JSON_API. It serves a deterministic patchedsix-1.16.0-py2.py3-none-any.whlunder uuid A (…0001,SOCKET_PATCHED = 1). It's then switched to offer only uuid B (…0004, a different patchedsix.py,SOCKET_PATCHED = 2) forpkg:pypi/six@1.16.0, and the old artifact URL stays served.Expected vs actual
updates[]lists exactly the UPGRADE rows the run acts on". For vendored mode, thescan --vendorparagraph 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)".updates[], then refuses it with a warning and exits 0 on the old uuid. Vendored refuses with exit 1.Matrix (Linux)
--dry-run)pypi_lock_source_already_exists)requirements.txt(uv pip)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 insame_hosted_artifactsince it was added in #358.Suspect code
crates/socket-patch-core/src/utils/python_script.rs:64(same_hosted_artifact) only accepts a different grant-token segment. A different patch uuid (current_path[grant_index + 1]) counts as a foreign source, sopython_script.rs:181returns "already declares a source … revert it before applying a different patch". A pin that is socket-patch's own hosted URL for the same name and version (on a recognised patch server) should be replaceable on an UPGRADE row.crates/socket-patch-core/src/vendor/pypi_uv.rs:372-381(pypi_uv_source_already_exists) refuses an existing socket-patch vendored source instead of re-vendoring it to the new uuid.utils/hatch.rs:147).Note: the vendored re-vendor also fails on a plain
requirements.txtproject (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.