[agent] Found by the scheduled uv bug-hunt routine (ledger #310).
Summary
Hosted rollback and remove can't restore a uv project whose patched package reaches a dependency group through a PEP 735 { include-group = "…" } member. uv's lock expands included groups, so the hosted rewrite repoints six's entry in both groups' requires-dev arrays. But the restore only reads the group's own string members from pyproject.toml. For the including group it finds no declaration and refuses: pyproject.toml no longer declares six. The hosted pin stays, and every later install keeps the patched wheel.
Vendored mode handles the same project: vendor --revert is byte-identical.
Impact
dev = [{ include-group = "test" }, { include-group = "lint" }] is the usual PEP 735 layout, and uv has supported it since dependency groups landed. In any such project, a hosted patch on a package from an included group can't be unwound with rollback or remove. Both exit 1 with the "restore it from version control" refusal, although nothing about the project changed after the scan.
Repro (uv 0.8.17, Linux, main 6e7ef74)
Uses a local mock of the patch API serving a patched six-1.16.0 wheel (the same mock as the earlier uv issues, embedded in the probe workflow below), plus SOCKET_PYPI_JSON_API pointing at a pypi.org forwarder.
cat > pyproject.toml <<'TOML'
[project]
name = "uvp"
version = "0.1.0"
requires-python = ">=3.9"
dependencies = ["python-dateutil==2.8.2"]
[dependency-groups]
test = ["six==1.16.0"]
dev = ["idna==3.7", {include-group = "test"}]
TOML
uv lock && git init -q && git add -A && git commit -qm init
socket-patch scan --mode hosted --json --yes $API # redirected: 1, uv sync --locked --all-groups installs the patch
socket-patch rollback --json --yes $API # exit 1, partial_failure
socket-patch remove pkg:pypi/six@1.16.0 --json --yes $API # exit 1, hosted_revert_failed
Output (rollback and remove alike):
cannot restore pkg:pypi/six@1.16.0 to its upstream registry entry: uv.lock: pyproject.toml no longer declares six, so the lock entry's specifier is not derivable; restore it from version control instead (`git checkout -- uv.lock`)
After the hosted scan, the lock repoints both the test entry and the expanded dev entry:
[package.metadata.requires-dev]
dev = [
{ name = "idna", specifier = "==3.7" },
{ name = "six", url = "http://127.0.0.1:18080/patch/pypi/six/…/six-1.16.0-py2.py3-none-any.whl" },
]
test = [{ name = "six", url = "…" }]
Controls: the same project with six listed directly in a group (no include-group), and with [tool.uv] default-groups = ["dev", "lint"], both roll back byte-identically. A self-referencing extra (all = ["uvp[test]"]) also rolls back fine.
Expected vs actual
- Expected: CLI_CONTRACT.md, "Hosted unwind coverage" (pypi): "Every file wiring the pin is rewritten back to the DEFAULT UPSTREAM registry entry … the root package's
requires-dist / requires-dev entries … re-derived from the paired metadata's declarations". The declaration exists. It's reachable through include-group, as uv itself resolves it. The refusal list (non-pure wheels, file filters, registry ambiguity, [[distribution]]) doesn't include this case.
- Actual: refused, exit 1.
uv.lock and pyproject.toml keep the hosted URL, and uv sync --locked --all-groups keeps installing the patched bytes.
OS × uv matrix
|
uv 0.5.31 |
uv 0.8.17 |
uv 0.12.21 |
| Linux (sandbox), rollback |
❌ |
❌ (rollback, remove) |
❌ |
| Linux (sandbox), vendored revert |
– |
✅ |
– |
| ubuntu-latest (probe): rollback / remove / vendored |
❌ / ❌ / ✅ |
– |
❌ / ❌ / ✅ |
| macos-latest (probe): rollback / remove / vendored |
❌ / ❌ / ✅ |
– |
❌ / ❌ / ✅ |
| windows-latest (probe): rollback / remove / vendored |
❌ / ❌ / ✅ |
– |
❌ / ❌ / ✅ |
First bad
Hosted upstream restore arrived in v5 (#277). The 4.0.0 release predates it, so there's nothing earlier to bisect.
Suspect code
crates/socket-patch-core/src/patch/redirect/upstream/uv.rs:716-725: the Declared::Dev(group) arm returns strings(dependency-groups[group]), which skips table members. It should expand { include-group = "<g>" } recursively, as PEP 735 and uv do. The vendored classifier also skips include-group members (vendor/pypi_uv.rs:274), but there it doesn't block the revert.
Probe: https://github.com/SocketDev/socket-patch/actions/runs/36878842786 (ubuntu, macos, windows × uv 0.5.31 / 0.12.21, all jobs print the same result)
[agent] Found by the scheduled uv bug-hunt routine (ledger #310).
Summary
Hosted
rollbackandremovecan't restore a uv project whose patched package reaches a dependency group through a PEP 735{ include-group = "…" }member. uv's lock expands included groups, so the hosted rewrite repoints six's entry in both groups'requires-devarrays. But the restore only reads the group's own string members frompyproject.toml. For the including group it finds no declaration and refuses:pyproject.toml no longer declares six. The hosted pin stays, and every later install keeps the patched wheel.Vendored mode handles the same project:
vendor --revertis byte-identical.Impact
dev = [{ include-group = "test" }, { include-group = "lint" }]is the usual PEP 735 layout, and uv has supported it since dependency groups landed. In any such project, a hosted patch on a package from an included group can't be unwound withrollbackorremove. Both exit 1 with the "restore it from version control" refusal, although nothing about the project changed after the scan.Repro (uv 0.8.17, Linux, main 6e7ef74)
Uses a local mock of the patch API serving a patched
six-1.16.0wheel (the same mock as the earlier uv issues, embedded in the probe workflow below), plusSOCKET_PYPI_JSON_APIpointing at a pypi.org forwarder.Output (rollback and remove alike):
After the hosted scan, the lock repoints both the
testentry and the expandeddeventry:Controls: the same project with six listed directly in a group (no include-group), and with
[tool.uv] default-groups = ["dev", "lint"], both roll back byte-identically. A self-referencing extra (all = ["uvp[test]"]) also rolls back fine.Expected vs actual
requires-dist/requires-deventries … re-derived from the paired metadata's declarations". The declaration exists. It's reachable throughinclude-group, as uv itself resolves it. The refusal list (non-pure wheels, file filters, registry ambiguity,[[distribution]]) doesn't include this case.uv.lockandpyproject.tomlkeep the hosted URL, anduv sync --locked --all-groupskeeps installing the patched bytes.OS × uv matrix
First bad
Hosted upstream restore arrived in v5 (#277). The 4.0.0 release predates it, so there's nothing earlier to bisect.
Suspect code
crates/socket-patch-core/src/patch/redirect/upstream/uv.rs:716-725: theDeclared::Dev(group)arm returnsstrings(dependency-groups[group]), which skips table members. It should expand{ include-group = "<g>" }recursively, as PEP 735 and uv do. The vendored classifier also skips include-group members (vendor/pypi_uv.rs:274), but there it doesn't block the revert.Probe: https://github.com/SocketDev/socket-patch/actions/runs/36878842786 (ubuntu, macos, windows × uv 0.5.31 / 0.12.21, all jobs print the same result)