Skip to content

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

Open

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.txt goes from six[foo] @ https://…/patch/pypi/six/…whl#sha256=… (patched) back to six[foo]==1.16.0 from PyPI (unpatched).
  • pylock.toml is wired to .socket/vendor/pypi/<uuid>/…whl.
  • The run prints Vendored 1 package. and exits 0. Its only signal is Warning: 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 PATCHED

Expected 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.txt can't be co-wired, the run should either refuse the purl (as the requirements-only eject does, with pypi_extras_unsupported) or keep the hosted pin in requirements.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.toml is wired, exit 0 / success, and the run's own vendor --check fails.

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): when sibling_pin_wirable is false, requirements.txt is pushed as a loser and the warning at :429 says 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 the pypi_extras_unsupported refusal the requirements-only flavor raises (:780).
  • crates/socket-patch-cli/src/commands/vendor.rs:1766 (run_eject) and the scan --mode vendored takeover: 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

  1. mikolalysenko commented on Oct 28, 2026

    @mikolalysenko
    CollaboratorAuthor

    [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

  2. mikolalysenko commented on Oct 28, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft PR: #1796


    Generated by Claude Code

  3. added 3 commits that reference this issue on Oct 28, 2026
    ab91716
    2d6811c
    8eafd68
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