[agent] Found by the scheduled pip / requirements.txt bug-hunt routine (ledger #309).
Summary
v5 hosted rollback, remove and the hosted → vendored takeover (scan --mode vendored over a hosted pin) all refuse a requirements.txt when every requirement line in it is a hosted pin. The simplest project shape, a single six==1.16.0 line, can be patched by socket-patch scan but can't be un-patched or vendored afterwards.
The refusal comes from the upstream restore. It tries to infer pip's hash-checking mode from the other requirement lines, and when there are none it gives up:
cannot restore pkg:pypi/[email protected] to its upstream registry entry: every requirement in requirements.txt is a hosted pin, so whether the original used pip's hash-checking mode (`--hash`) is not derivable; restore it from version control instead (`git checkout -- requirements.txt`)
The same refusal fires when the only other lines are options or editables. For example, -e . plus a pin is refused, even though -e lines are incompatible with hash-checking mode, so the original must have been unhashed.
Impact
socket-patch rollback and socket-patch remove <purl> exit 1 (partial_failure / hosted_revert_failed) on single-dependency projects, on projects where every pin is patched, and on -e . + pin projects.
socket-patch scan --mode vendored on such a hosted project can't take over: Vendored 0 packages; 1 failed., exit 1, and the project stays hosted. The documented mode-takeover path (CLI_CONTRACT.md, "Takeover reconciliation (every hosted ecosystem, v5.0)") is unusable for these projects.
- v5 keeps no hosted ledger, so the CLI can't undo the rewrite at all. The only remedy is version control.
Repro
Uses a local mock of the patch API that serves a patched six-1.16.0 wheel. It's the same shape as tests/vex_pypi_real_common::RealApi: POST /v0/orgs/test-org/patches/batch, /patches/package grant, /patches/view/<uuid>, and the wheel route. Linux, pip 24.0, CPython 3.11, main 2463257.
A="--api-url http://127.0.0.1:8765 --api-token fake --org test-org --patch-server-url http://127.0.0.1:8765"
mkdir p && cd p && printf 'six==1.16.0\n' > requirements.txt
socket-patch scan $A # Switched 1 package to hosted patches; rewrote 1 file.
socket-patch rollback --yes $A; echo $? # Error: Cannot restore … not derivable … → 1
socket-patch remove pkg:pypi/[email protected] --yes $A; echo $? # same error → 1
socket-patch scan --mode vendored $A; echo $? # Cannot vendor … not derivable … Vendored 0 packages; 1 failed. → 1
Variants I checked (scan, then rollback):
| requirements.txt before scan |
rollback |
six==1.16.0 |
refused |
-e . + six==1.16.0 |
refused |
six==1.16.0 --hash=… --hash=… (only line) |
refused |
six==1.16.0 + idna==3.7 |
restored |
--require-hashes + hashed six |
restored |
--index-url … + six==1.16.0 + idna==3.7 |
restored |
pip-compile hashed idna + six (LF and CRLF) |
restored byte-exact |
Expected vs actual
- Expected: CLI_CONTRACT.md, "Hosted unwind coverage", says every file wiring a pin is rewritten back to the default upstream entry. The pypi bullet lists what gets refused:
pdm.lock without cross_platform, non-pure uv / pylock wheels, uv option filters, and uv 0.2 locks. An all-hosted requirements.txt isn't in that list. When no other requirement constrains the mode, either restored form, six==1.16.0 or six==1.16.0 --hash=… for every release file, installs with every pip, because there is no other line it could conflict with. A -e line also settles the mode as unhashed.
- Actual: the pin is refused in
rollback, remove and the takeover.
OS × version
| OS |
pip / Python |
reproduces |
| Linux (sandbox) |
24.0 / 3.11 (CLI-only; no pip step involved) |
yes, twice |
| ubuntu-latest (probe) |
20.3.4 / 3.8 |
yes (rollback, remove and takeover all refused, exit 1) |
| ubuntu-latest (probe) |
25.0.1 / 3.8 |
yes (rollback, remove and takeover all refused, exit 1) |
| ubuntu-latest (probe) |
26.2.1 / 3.13 |
yes (rollback, remove and takeover all refused, exit 1) |
| macos-latest (probe) |
20.3.4 / 3.8 |
yes (rollback, remove and takeover all refused, exit 1) |
| macos-latest (probe) |
25.0.1 / 3.8 |
yes (rollback, remove and takeover all refused, exit 1) |
| macos-latest (probe) |
26.2.1 / 3.13 |
yes (rollback, remove and takeover all refused, exit 1) |
| windows-latest (probe) |
20.3.4 / 3.8 |
yes (rollback, remove and takeover all refused, exit 1) |
| windows-latest (probe) |
25.0.1 / 3.8 |
yes (rollback, remove and takeover all refused, exit 1) |
| windows-latest (probe) |
26.2.1 / 3.13 |
yes (rollback, remove and takeover all refused, exit 1) |
| all 3 OS (probe) |
20.3.4 / 3.13 |
blocked: pip 20.3.4 can't run on 3.13 (no distutils) |
The refusal is decided before any pip runs, so it doesn't depend on the pip version.
First bad
The code is new in 2463257 (#277), which replaced v4's ledger replay with the upstream restore. I didn't get a clean comparison: on v4.0.0, rollback of the same hosted project stopped at Manifest not found even though .socket/vendor/redirect-state.json existed.
Suspect code
Probe run: https://github.com/SocketDev/socket-patch/actions/runs/36806309982
[agent] Found by the scheduled pip / requirements.txt bug-hunt routine (ledger #309).
Summary
v5 hosted
rollback,removeand the hosted → vendored takeover (scan --mode vendoredover a hosted pin) all refuse arequirements.txtwhen every requirement line in it is a hosted pin. The simplest project shape, a singlesix==1.16.0line, can be patched bysocket-patch scanbut can't be un-patched or vendored afterwards.The refusal comes from the upstream restore. It tries to infer pip's hash-checking mode from the other requirement lines, and when there are none it gives up:
The same refusal fires when the only other lines are options or editables. For example,
-e .plus a pin is refused, even though-elines are incompatible with hash-checking mode, so the original must have been unhashed.Impact
socket-patch rollbackandsocket-patch remove <purl>exit 1 (partial_failure/hosted_revert_failed) on single-dependency projects, on projects where every pin is patched, and on-e .+ pin projects.socket-patch scan --mode vendoredon such a hosted project can't take over:Vendored 0 packages; 1 failed., exit 1, and the project stays hosted. The documented mode-takeover path (CLI_CONTRACT.md, "Takeover reconciliation (every hosted ecosystem, v5.0)") is unusable for these projects.Repro
Uses a local mock of the patch API that serves a patched
six-1.16.0wheel. It's the same shape astests/vex_pypi_real_common::RealApi:POST /v0/orgs/test-org/patches/batch,/patches/packagegrant,/patches/view/<uuid>, and the wheel route. Linux, pip 24.0, CPython 3.11, main2463257.Variants I checked (scan, then rollback):
six==1.16.0-e .+six==1.16.0six==1.16.0 --hash=… --hash=…(only line)six==1.16.0+idna==3.7--require-hashes+ hashedsix--index-url …+six==1.16.0+idna==3.7idna+six(LF and CRLF)Expected vs actual
pdm.lockwithoutcross_platform, non-pure uv / pylock wheels, uv option filters, and uv 0.2 locks. An all-hostedrequirements.txtisn't in that list. When no other requirement constrains the mode, either restored form,six==1.16.0orsix==1.16.0 --hash=…for every release file, installs with every pip, because there is no other line it could conflict with. A-eline also settles the mode as unhashed.rollback,removeand the takeover.OS × version
distutils)The refusal is decided before any pip runs, so it doesn't depend on the pip version.
First bad
The code is new in
2463257(#277), which replaced v4's ledger replay with the upstream restore. I didn't get a clean comparison: on v4.0.0,rollbackof the same hosted project stopped atManifest not foundeven though.socket/vendor/redirect-state.jsonexisted.Suspect code
crates/socket-patch-core/src/patch/redirect/upstream/pypi.rs:597-603: the(false, false)arm ofhash_mode.--prefixed lines,-eincluded, are skipped before the mode tally (if code.starts_with('-') { continue; }).pip install -rfor every other unhashed line #376. The hosted writer always emits--hash, so the line itself never records the original mode. Once Hosted and vendored requirements.txt rewrites add a --hash to one line, which turns on pip's hash-checking mode and breakspip install -rfor every other unhashed line #376 / Fix requirements.txt writers ignoring pip hash mode (#376) #383 switch unhashed files to a#sha256=fragment, the hosted line's own shape (--hashvs fragment) would say which mode to restore.Probe run: https://github.com/SocketDev/socket-patch/actions/runs/36806309982