Skip to content

Hosted uv rollback and remove refuse when the patched package reaches a dependency group through PEP 735 include-group #473

Description

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

Activity

  1. mikolalysenko commented on Oct 1, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged as priority:p1 (uv). No duplicate or open fix PR found.


    Generated by Claude Code

  2. mikolalysenko commented on Oct 3, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (with #606; shared root cause: the hosted uv unwind matches a lock requirement entry to its pyproject declaration by name over a flattened declaration set, instead of following uv's own lowering: extras/markers per entry, include-group expansion). Branch: agent/fix-uv-unwind-declaration-match. Claim-ID: 2026-10-03T00:20:52Z-f048a3


    Generated by Claude Code

  3. mikolalysenko commented on Oct 3, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft PR: #625


    Generated by Claude Code

  4. mikolalysenko commented on Oct 5, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Janitor: closing as completed. PR #625 (merge 9df2afa5, "Fixes #606 and #473") merged into main. GitHub auto-closed only #606. On origin/main 6811b4e7, patch/redirect/upstream/uv.rs now expands PEP 735 { include-group = … } members when it matches the lock entry to its declaration (~L880–907), and the unit test include_group_member (~L1905, "#473") plus nested_and_cyclic_include_groups cover it. The e2e test hosted_uv_include_group_manifestless_vex in tests/e2e_redirect_uv_build.rs covers it end to end. Reopen if it still reproduces.


    Generated by Claude Code

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