Skip to content

Hosted → vendored over requirements.txt + a pip lock pylock.toml un-hosts both files but wires only pylock.toml, so pip install -r requirements.txt goes from patched to unpatched while vendor exits 0 #1758

Closed

Description

[agent] Found by the scheduled pip / requirements.txt bug-hunt routine (ledger #309).

Summary

Take a pip project that installs from both requirements.txt and a pip lock pylock.toml. scan --mode hosted pins the patched wheel in both files. A later hosted → vendored switch, either plain socket-patch vendor (the #1648 eject) or scan --mode vendored, restores both files to their upstream registry entry, but then wires the vendored wheel into pylock.toml only. requirements.txt ends up back at six==1.16.0 from PyPI. Before the switch, that install path got the patched wheel; after it, it gets the unpatched release. The run prints Vendored 1 package. and exits 0 (status: success). The only signal is a pypi_multiple_lockfiles warning that says installs driven by requirements.txt retain their existing sources. That's false here: the run itself just replaced that source.

The same project's own vendor --check, run straight afterwards, fails (exit 1) with wiring contested: pylock.toml wires .socket/vendor/pypi/<uuid>, but requirements.txt resolves the same version from elsewhere (not a Socket patch), so an install driven by requirements.txt gets the unpatched package.

Impact

  • A hosted → vendored migration silently drops the patch from every pip install -r requirements.txt consumer (CI, Dockerfiles, deploy scripts), while the CLI reports success. Real pip 26.2.2 confirms it below.
  • The run breaks the "a takeover the vendored backend does not carry through keeps the hosted pin" guarantee for one of the two files it un-hosts.
  • vendor --check goes from nothing to check to failed on the same commit, with a remedy ("delete whichever of the two locks") the user didn't need before.

Repro

This uses the repo's own fixtures: a wiremock hosted API as in tests/mode_migration_pypi.rs::mount_hosted_api, plus prebuilt_common::mount_view with a PyPI JSON mock for the eject, as in tests/vendor_eject_fresh_checkout.rs::pypi_eject_with_virtualenv. requirements.txt and pylock.toml are real files: the second was made by pip lock -r requirements.txt -o pylock.toml (pip 26.2.2). There's no virtualenv (lock-only).

printf 'idna==3.7\nsix==1.16.0\n' > requirements.txt
pip lock -r requirements.txt -o pylock.toml        # pip 26.2.2
socket-patch scan --mode hosted --yes                # mock API grants six@1.16.0
# requirements.txt: six @ https://patch.socket.dev/patch/pypi/six/1.16.0/…/six-1.16.0-py2.py3-none-any.whl#sha256=…
# pylock.toml:      archive = { url = "https://patch.socket.dev/patch/pypi/six/…whl", hashes = { sha256 = "…" } }
socket-patch vendor                                  # or: socket-patch scan --mode vendored --yes
socket-patch vendor --check

vendor (exit 0):

Ejecting 1 hosted package into .socket/vendor/...
Vendored 1 package.
Warning: wiring pylock.toml; installs driven by requirements.txt retain their existing sources

After it, requirements.txt reads idna==3.7\nsix==1.16.0\n (hosted pin gone, nothing vendored), and pylock.toml has archive = { path = ".socket/vendor/pypi/<uuid>/six-1.16.0-py3-none-any.whl", … }.

scan --mode vendored --yes gives the same files and prints Note: pkg:pypi/six@1.16.0 was hosted; restored its upstream registry entry (pylock.toml, requirements.txt) before vendoring (mode takeover), followed by the same warning.

vendor --check (exit 1):

pkg:pypi/six@1.16.0: wiring contested: pylock.toml wires .socket/vendor/pypi/2b1f6c1e-…, but requirements.txt resolves the same version from elsewhere (not a Socket patch), so an install driven by requirements.txt gets the unpatched package; …

Real pip 26.2.2 / CPython 3.11, each in a fresh venv:

  • pip install -r requirements.txt: six.py is the upstream PyPI file (unpatched).
  • pip install -r pylock.toml: six.py is the patched file (SAFE = True).

Expected vs actual

Matrix (Linux; OS-independent code path)

requirements.txt before hosting pylock.toml path result
idna==3.7 + six==1.16.0 (hosted → six @ …#sha256=) pip lock output, 2 packages plain vendor (eject) fail (reproduced 3/3)
same same scan --mode vendored --yes fail
pip-compile style, hash continuations (hosted → six @ … --hash=) same plain vendor fail
requirements.txt only (no pylock) none plain vendor pass (wired)
pylock.toml only pip lock output plain vendor pass

Bisect: not a #1648 regression. The plain-vendor eject at #1648's parent 8244b94 gives the same result (both files un-hosted, only pylock.toml wired, vendor --check contested), and so does the scan --mode vendored takeover on main b787454. #1648 does make plain vendor the default way to leave hosted mode, even when .socket/manifest.json exists, so more projects now go through this path.

Suspect code

  • crates/socket-patch-core/src/vendor/pypi.rs:389-433 (detect_pypi_flavor): when a pylock holds the target, requirements.txt is pushed as a loser of the PythonLocks flavor. The retain their existing sources clause assumes the file wasn't touched, but in a takeover or eject the upstream restore has already rewritten it.
  • crates/socket-patch-cli/src/commands/vendor.rs:1716 (run_eject) and the takeover in scan --mode vendored: the restore covers every hosted file (restore_upstream over all pins), but nothing checks that the vendored backend re-wires each restored file. A file the backend leaves as a loser should keep its hosted pin, or the purl should be refused.

No probe runs: the code path is OS-independent and was reproduced on Linux only (probe branches are still blocked for this routine).

Activity

  1. added
    bugSomething isn't working
    bughuntFound by a scheduled package-manager bug-hunt agent
    pm:pippip / requirements.txt
    on Oct 28, 2026
  2. mikolalysenko commented on Oct 28, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged: priority:p1 (pip / pylock). Not a duplicate: #612 and #1368 fixed the same gap for a requirements.txt exported beside Pipfile.lock / uv.lock (wired with the lock via sibling_pin_wirable), but the PythonLocks flavor in detect_pypi_flavor (crates/socket-patch-core/src/vendor/pypi.rs) still lists requirements.txt as a loser, so the takeover/eject restore un-hosts it and nothing re-wires it. No open PR covers it.


    Generated by Claude Code

  3. mikolalysenko commented on Oct 28, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (shared root cause: the PythonLocks vendored flavor treats a requirements.txt beside a pylock as a loser instead of wiring its pin with the lock, so a hosted→vendored restore un-hosts it). Branch: agent/fix-pylock-requirements-sibling. Claim-ID: 2026-10-28T02:21:14Z-452f40


    Generated by Claude Code

  4. mikolalysenko commented on Oct 28, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft PR: #1759


    Generated by Claude Code

  5. added 3 commits that reference this issue on Oct 28, 2026
    701b87b
    46e40d2
    09523b8
  6. mikolalysenko commented on Oct 28, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Verified fixed on main b4b6d98 (#1759) with real pip 26.2.2 / CPython 3.11. I used idna==3.7 + six==1.16.0, pip lock -r requirements.txt -o pylock.toml, then scan --mode hosted. After that, both plain vendor and scan --mode vendored --yes wire requirements.txt (./.socket/vendor/pypi/<uuid>/six-…whl # socket-patch vendor: six==1.16.0) and pylock.toml, and vendor --check passes. pip install -r of either file installs the patched six.py. vendor --revert is byte-identical to the pre-hosted commit. A -r req/base.txt include, a marker pin, and a pip-compile CRLF --hash file beside pylock.dev.toml also pass.

    One shape is still affected: an extras pin (six[foo]==1.16.0), which sibling_pin_wirable rejects. Filed as #1794.


    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

    agent:triagedbugSomething isn't workingbughuntFound by a scheduled package-manager bug-hunt agentpm:pippip / requirements.txtpriority:p1

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions