Repository navigation
Hosted → vendored over an extras pin (six[foo]==X) in requirements.txt beside a pip lock pylock.toml still un-hosts requirements.txt and leaves it unpatched, exit 0 (#1758 residual) #1794
Description
[agent] Found by the scheduled pip / requirements.txt bug-hunt routine (ledger #309).
Summary
#1759 fixed #1758 for an exact six==1.16.0 pin: a hosted → vendored switch now wires requirements.txt together with the pip lock pylock.toml. It does that only when sibling_pin_wirable accepts the pin. A pin the requirements wiring can't express, such as one with extras (six[foo]==1.16.0), is still left as a pypi_multiple_lockfiles "loser". The upstream restore has already un-hosted it, though. So after plain socket-patch vendor (the eject) or scan --mode vendored --yes:
requirements.txtgoes fromsix[foo] @ https://…/patch/pypi/six/…whl#sha256=…(patched) back tosix[foo]==1.16.0from PyPI (unpatched).pylock.tomlis wired to.socket/vendor/pypi/<uuid>/…whl.- The run prints
Vendored 1 package.and exits 0. Its only signal isWarning: wiring pylock.toml; installs driven by requirements.txt retain their existing sources, which is false: the run just replaced that source. vendor --check, run straight afterwards, fails (exit 1):wiring contested: pylock.toml wires .socket/vendor/pypi/<uuid>, but requirements.txt resolves the same version from elsewhere (not a Socket patch) ….
The same hosted extras pin without a pylock is handled correctly. The eject refuses it (requirements.txt: the six pin declares extras, which a vendored wheel path line cannot express …, exit 1), and the hosted pin stays in place.
Impact
Any pip install -r requirements.txt consumer (CI, Dockerfiles) of a project with an extras pin (e.g. requests[socks]==…) and a pylock silently goes from patched to unpatched during a hosted → vendored migration, while the CLI reports success.
Repro
This uses a local mock of the patch API: /v0/orgs/<org>/patches/{batch,by-package,view,package} grants pkg:pypi/six@1.16.0, and the grant's tarball artifact is a patched six wheel (SAFE = True appended to six.py). The same fixtures appear in tests/mode_migration_pypi.rs and tests/vendor_eject_fresh_checkout.rs. Real pip 26.2.2 / CPython 3.11; idna and six come from real PyPI.
git init -q
printf 'idna==3.7\nsix[foo]==1.16.0\n' > requirements.txt
pip lock -r requirements.txt -o pylock.toml
git add -A && git commit -qm init
socket-patch scan --mode hosted --yes # "Switched 1 package to hosted patches; rewrote 2 files."
# requirements.txt: six[foo] @ http://…/patch/pypi/six/1.16.0/…/six-1.16.0-py2.py3-none-any.whl#sha256=…
# fresh venv: pip install -r requirements.txt -> six.py PATCHED
socket-patch vendor # or: socket-patch scan --mode vendored --yes
# Ejecting 1 hosted package into .socket/vendor/...
# Warning: wiring pylock.toml; installs driven by requirements.txt retain their existing sources
# Vendored 1 package. (exit 0)
cat requirements.txt # idna==3.7 / six[foo]==1.16.0
socket-patch vendor --check # exit 1, "wiring contested: … requirements.txt resolves the same version from elsewhere"
# fresh venv: pip install -r requirements.txt -> six.py UNPATCHED (upstream PyPI wheel)
# fresh venv: pip install -r pylock.toml -> six.py PATCHEDExpected vs actual
- Expected (crates/socket-patch-cli/CLI_CONTRACT.md, "Takeover reconciliation"): "A takeover the vendored backend does not carry through keeps the hosted pin (pnpm hosted-to-vendored scan/get dry-run previews success for a refused takeover #853, uv scan/get dry-run does not predict a refused hosted-to-vendored takeover #944) … the restore is rolled back before anything reaches disk … so the package stays hosted-patched instead of being un-hosted and then refused." When the restored
requirements.txtcan't be co-wired, the run should either refuse the purl (as the requirements-only eject does, withpypi_extras_unsupported) or keep the hosted pin inrequirements.txt, and in both cases report it. At the very least, the warning shouldn't claim the file keeps its existing source. - Actual: both files are un-hosted, only
pylock.tomlis wired, exit 0 /success, and the run's ownvendor --checkfails.
Matrix (Linux, main b4b6d98; the code path is OS-independent)
| requirements.txt pin (hosted first) | pylock.toml | switch | result |
|---|---|---|---|
six[foo]==1.16.0 |
pip lock output |
plain vendor (eject) |
fail (reproduced 3/3) |
six[foo]==1.16.0 |
pip lock output |
scan --mode vendored --yes |
fail |
six[foo]==1.16.0 |
none | plain vendor |
pass (refused pypi_extras_unsupported, hosted pin kept, exit 1) |
six==1.16.0 |
pip lock output |
plain vendor / scan --mode vendored |
pass (#1759: both wired, pip installs patched from both) |
six==1.16.0 ; python_version >= "3" |
pip lock output |
plain vendor |
pass |
pip-compile CRLF, two --hash + # via |
pylock.dev.toml |
plain vendor, then vendor --revert |
pass |
First bad: present since the eject landed (#1648). Before #1759, every shape failed this way (#1758). #1759 narrowed it to the pins sibling_pin_wirable rejects.
Suspect code
crates/socket-patch-core/src/vendor/pypi.rs:390(detect_pypi_flavor): whensibling_pin_wirableis false,requirements.txtis pushed as a loser and the warning at:429says it "retain[s] their existing sources". That assumption doesn't hold after the takeover / eject restore rewrote the file.crates/socket-patch-core/src/vendor/pypi_requirements.rs:367(sibling_pin_wirable): extras (and any other shape the wiring can't rewrite) return false, so they fall through silently instead of producing thepypi_extras_unsupportedrefusal the requirements-only flavor raises (:780).crates/socket-patch-cli/src/commands/vendor.rs:1766(run_eject) and thescan --mode vendoredtakeover: nothing checks that every file the restore un-hosted is re-wired by the backend.
The same loser path probably applies to a uv.lock / Pipfile.lock sibling (#612 / #1368) with an extras pin in the exported requirements.txt. That wasn't tested here (other routines' scope).
No probe runs: this routine's probe branches are still blocked; reproduced on Linux only.
Activity
mikolalysenko commentedon Oct 28, 2026
[agent] Claiming this issue (shared root cause: a requirements.txt sibling the vendored wiring can't express is left as an un-hosted loser after the takeover restore). Branch: agent/fix-pypi-unwirable-sibling-pin. Claim-ID: 2026-10-28T20:21:28Z-d16ee3
Generated by Claude Code
mikolalysenko commentedon Oct 28, 2026
[agent] Draft PR: #1796
Generated by Claude Code
Refuse an extras pin beside a pylock
Refuse extras pins only on a fresh lock wiring
Refuse a fresh lock wiring beside an unpinned extras pin