[agent] Found by the scheduled pip / requirements.txt bug-hunt routine (ledger #309).
Summary
pip lock (pip 25.1+) writes a PEP 751 pylock.toml, and pip 26.1+ installs from it with pip install -r pylock.toml. Unlike uv, pip writes each artifact as an array of tables:
[[packages]]
name = "idna"
version = "3.7"
[[packages.wheels]]
name = "idna-3.7-py3-none-any.whl"
url = "https://files.pythonhosted.org/packages/…/idna-3.7-py3-none-any.whl"
[packages.wheels.hashes]
sha256 = "82fe…"
scan --mode hosted handles this file fine. It rewrites six to archive = { url = <hosted wheel>, hashes = … }, and pip 26.2.1 installs the patched wheel from it. But the upstream restore can't read pip's shape: artifact_tables and the shape probe only accept an inline wheels array. Every sibling (here, idna with only files.pythonhosted.org wheels) is then skipped as "not a registry package". So rollback and remove always refuse:
cannot restore pkg:pypi/six@1.16.0 to its upstream registry entry: pylock.toml: no sibling registry package shows the registry and artifact fields this uv release records; restore it from version control instead (`git checkout -- pylock.toml`)
They exit 1 (partial_failure / hosted_revert_failed), and rollback --dry-run predicts the same failure. The same lock with the same siblings written as inline wheels = [{…}] rolls back with exit 0 (control below). This is a new shape, not #407: #407 (fixed) was the uv pip compile pylock that has no index. pip's lock has no index either, and that part is now handled, but its siblings are never recognised.
Impact
On any project that uses pip's own lock format, a hosted patch can be applied but never unpatched with socket-patch rollback / remove, the same class as #410 for requirements.txt. The error message also blames "this uv release" for a file that created-by = "pip" wrote. The hosted → vendored takeover goes through the same upstream restore and is probably refused too; I haven't verified that.
Repro (Linux, main 045d7ec)
The hosted API is a local wiremock: the mount_hosted_api routes from crates/socket-patch-cli/tests/mode_migration_pypi.rs, served by a throwaway, uncommitted driver. The upstream restore uses the real PyPI JSON API.
python3 -m venv v && v/bin/pip install pip==26.2.1 # also: pip==25.1, 25.3, 26.0 (same file shape)
printf 'six==1.16.0\nidna==3.7\n' > requirements.txt
v/bin/pip lock -r requirements.txt -o pylock.toml # created-by = "pip", [[packages.wheels]] tables
rm requirements.txt
F="--yes --patch-server-url $MOCK"
socket-patch scan --mode hosted --json --api-url $MOCK --org test-org --api-token fake $F
# exit 0, redirected 1, six -> archive = { url = "$MOCK/patch/pypi/six/1.16.0/…/six-1.16.0-py2.py3-none-any.whl", hashes = … }
python3 -m venv f && f/bin/pip install pip==26.2.1 && f/bin/pip install -r pylock.toml # installs the patched six (ok)
socket-patch rollback $F # exit 1: "no sibling registry package shows the registry and artifact fields this uv release records"
socket-patch remove pkg:pypi/six@1.16.0 $F # exit 1: hosted_revert_failed, same message
socket-patch rollback --dry-run --json $F # partial_failure, same message
Control: rewrite the same original lock with inline wheels = [{ name, url, hashes }] for both packages. scan --mode hosted then rollback exits 0 with hosted.reverted: ["pkg:pypi/six@1.16.0"].
Reproduced twice: once on a pip 26.2.1 lock and once on a separate project with a pip 25.1 lock.
Expected vs actual
CLI_CONTRACT.md (the rollback upstream-restore list) refuses only a pylock "whose other registry packages show neither an index nor (as uv pip compile writes them) only PyPI files with none, which restores the entry without an index too". The pip lock's sibling has no index and only PyPI files, so it should restore without an index. PEP 751 allows both TOML spellings of packages.wheels.
Expected: rollback / remove restore six's [[packages.wheels]] entry from PyPI (in the siblings' array-of-tables shape) and exit 0. Or, if that shape isn't supported, scan --mode hosted should refuse it up front instead of writing a patch it can't undo. Actual: the scan succeeds, and every rollback / remove exits 1.
OS × version
| OS |
pip that wrote the lock |
hosted scan + pip install -r pylock.toml |
rollback / remove |
| Linux |
26.2.1 (py3.11) |
pass (pip 26.2.1 installs the patched wheel) |
fail |
| Linux |
25.1 (py3.11) |
pass (scan; pip < 26.1 can't install a pylock) |
fail |
| Linux |
25.3, 26.0 |
same [[packages.wheels]] shape (not run end to end) |
expected fail |
| macOS / Windows |
— |
not probed (TOML parsing only, OS-independent) |
not probed |
First bad version
Not bisected. The array-of-tables shape has never been readable: the restore was written against uv's inline output, and the only [[packages.wheels]] fixture in the repo (tests/e2e_vex_lockfile/uv.rs:198) is a vex test.
Suspect code
crates/socket-patch-core/src/patch/redirect/upstream/uv.rs:328 artifact_tables: package.get("wheels").and_then(Item::as_array) (and sdist via as_value) returns nothing for an ArrayOfTables / standard table. So pylock_unindexed_registry (:353) returns None for every pip sibling.
uv.rs:407-426, the shape probe: same as_array / as_inline_table assumption.
uv.rs:442: the resulting refusal, worded for uv.
[agent] Found by the scheduled pip / requirements.txt bug-hunt routine (ledger #309).
Summary
pip lock(pip 25.1+) writes a PEP 751pylock.toml, and pip 26.1+ installs from it withpip install -r pylock.toml. Unlike uv, pip writes each artifact as an array of tables:scan --mode hostedhandles this file fine. It rewrites six toarchive = { url = <hosted wheel>, hashes = … }, and pip 26.2.1 installs the patched wheel from it. But the upstream restore can't read pip's shape:artifact_tablesand the shape probe only accept an inlinewheelsarray. Every sibling (here,idnawith onlyfiles.pythonhosted.orgwheels) is then skipped as "not a registry package". Sorollbackandremovealways refuse:They exit 1 (
partial_failure/hosted_revert_failed), androllback --dry-runpredicts the same failure. The same lock with the same siblings written as inlinewheels = [{…}]rolls back with exit 0 (control below). This is a new shape, not #407: #407 (fixed) was theuv pip compilepylock that has noindex. pip's lock has noindexeither, and that part is now handled, but its siblings are never recognised.Impact
On any project that uses pip's own lock format, a hosted patch can be applied but never unpatched with
socket-patch rollback/remove, the same class as #410 for requirements.txt. The error message also blames "this uv release" for a file thatcreated-by = "pip"wrote. The hosted → vendored takeover goes through the same upstream restore and is probably refused too; I haven't verified that.Repro (Linux, main
045d7ec)The hosted API is a local wiremock: the
mount_hosted_apiroutes fromcrates/socket-patch-cli/tests/mode_migration_pypi.rs, served by a throwaway, uncommitted driver. The upstream restore uses the real PyPI JSON API.Control: rewrite the same original lock with inline
wheels = [{ name, url, hashes }]for both packages.scan --mode hostedthenrollbackexits 0 withhosted.reverted: ["pkg:pypi/six@1.16.0"].Reproduced twice: once on a pip 26.2.1 lock and once on a separate project with a pip 25.1 lock.
Expected vs actual
CLI_CONTRACT.md (the rollback upstream-restore list) refuses only a pylock "whose other registry packages show neither an
indexnor (asuv pip compilewrites them) only PyPI files with none, which restores the entry without anindextoo". The pip lock's sibling has noindexand only PyPI files, so it should restore without anindex. PEP 751 allows both TOML spellings ofpackages.wheels.Expected:
rollback/removerestore six's[[packages.wheels]]entry from PyPI (in the siblings' array-of-tables shape) and exit 0. Or, if that shape isn't supported,scan --mode hostedshould refuse it up front instead of writing a patch it can't undo. Actual: the scan succeeds, and every rollback / remove exits 1.OS × version
pip install -r pylock.toml[[packages.wheels]]shape (not run end to end)First bad version
Not bisected. The array-of-tables shape has never been readable: the restore was written against uv's inline output, and the only
[[packages.wheels]]fixture in the repo (tests/e2e_vex_lockfile/uv.rs:198) is a vex test.Suspect code
crates/socket-patch-core/src/patch/redirect/upstream/uv.rs:328artifact_tables:package.get("wheels").and_then(Item::as_array)(andsdistviaas_value) returns nothing for anArrayOfTables/ standard table. Sopylock_unindexed_registry(:353) returnsNonefor every pip sibling.uv.rs:407-426, the shape probe: sameas_array/as_inline_tableassumption.uv.rs:442: the resulting refusal, worded for uv.