Vendored mode in a Pipenv project wires only Pipfile.lock and silently leaves a sibling requirements.txt unpatched, and the hosted → vendored takeover reverts that file's hosted pin to plain PyPI #612
Description
[agent] Found by the scheduled Pipenv bug-hunt routine (ledger #313).
Summary
A Pipenv project often has a root requirements.txt exported by pipenv requirements (or pipenv lock -r on 2018) for Docker or plain-pip installs. Hosted mode rewires both files; that's been the case since the #333 fix. Vendored mode rewires only Pipfile.lock:
scan --mode vendored/vendorleavessix==1.16.0inrequirements.txtuntouched. It reportsstatus: success, exit 0, and emits no warning.vendor --checkthen reportsverified: 1, success.pip install -r requirements.txtinstalls the unpatched upstream six.vexsees the conflict and refuses to attest (patched_ref_unattributable, exit 1). Its remedy says "rewire both locks (re-runsocket-patch scan/vendor)", but re-running givesalready_vendoredand changes nothing. So the user can't get the project attested.- The hosted → vendored takeover is worse. Start from a hosted project where both files carry the hosted URL.
scan --mode vendoredreportsvendor_takeover_reverted_redirect"restored its upstream registry entry (Pipfile.lock, requirements.txt)", then vendors only Pipfile.lock. So the mode switch turns a patchedrequirements.txtback intosix==1.16.0from PyPI, and still reports success.
The vendored flavor router detect_pypi_flavor (crates/socket-patch-core/src/vendor/pypi.rs:248) returns PypiFlavor::Pipenv as soon as Pipfile.lock exists (:333). requirements.txt only counts as a competing install source on the standalone-pylock branch (:286), so the pypi_multiple_lockfiles warning (:312) never fires for Pipfile.lock + requirements.txt.
Impact
Teams that build containers with pipenv requirements > requirements.txt && pip install -r requirements.txt ship the unpatched package after a successful vendored run, and vendor --check (the CI gate) passes. A user switching an existing hosted project to vendored loses the patch on the pip path without any signal except a later VEX refusal.
Repro (Linux; real Pipenv; local mock patch API serving a patched six 1.16.0 wheel)
mkdir app && cd app
cat > Pipfile <<'EOF'
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
six = "==1.16.0"
idna = "==3.7"
[requires]
python_version = "3.12"
EOF
pipenv lock
pipenv requirements > requirements.txt # 2018.11.26: pipenv lock -r > requirements.txt
socket-patch scan --mode vendored --yes --json # status success, exit 0, no warning
grep six requirements.txt # six==1.16.0; python_version >= ... (unchanged)
grep -c socket/vendor Pipfile.lock # 1
socket-patch vendor --check --json # success, verified: 1
uv venv rv && uv pip install -p rv/bin/python -r requirements.txt
rv/bin/python -c "import six; print(getattr(six,'SOCKET_PATCHED','UNPATCHED'))" # UNPATCHED
pipenv sync && pipenv run python -c "import six; print(six.SOCKET_PATCHED)" # 1
socket-patch vex --product pkg:pypi/app@1.0.0 # exit 1: patched_ref_unattributable, "re-run scan / vendor"
socket-patch scan --mode vendored --yes --json # already_vendored; requirements.txt still unpatched
# Takeover variant (fresh copy of the same project)
socket-patch scan --mode hosted --yes --json # rewrittenFiles: [Pipfile.lock, requirements.txt]
socket-patch scan --mode vendored --yes --json # success; vendor_takeover_reverted_redirect (Pipfile.lock, requirements.txt)
cat requirements.txt # six==1.16.0 ; python_version ... <- hosted pin goneEvery cell below was reproduced twice.
Expected vs actual
- Expected: one of these:
- Vendored wires every install source that pins the package, as hosted does. Plain
requirements.txtvendoring is supported (docs/ecosystems.md: "pipenv … and requirements.txt"), and works on the same file without a Pipfile.lock. - Or, at minimum, the documented warning. CLI_CONTRACT.md:1179: "
pypi_multiple_lockfiles… a sibling lockfile of another package manager will still install UNPATCHED bytes; names the wired winner + the ignored locks". Thedetect_pypi_flavordoc comment promises this "LOUD" warning, because such files otherwise "go stale-but-valid, which is otherwise invisible". - A takeover should never leave a file less patched than it found it.
- Vendored wires every install source that pins the package, as hosted does. Plain
- Actual: silent success,
vendor --checkgreen, the pip path unpatched. The takeover actively unpatchesrequirements.txt, and the VEX remedy text can't be followed.
OS × version (Linux, main 045d7ec)
| Pipenv | vendored skips requirements.txt, no warning | vendor --check green |
pip install -r unpatched |
vex refuses | hosted → vendored unpatches requirements.txt |
|---|---|---|---|---|---|
2018.11.26 (py3.8, lock -r) |
✅ repro 2/2 | ✅ | ✅ | ✅ | not run |
| 2023.12.1 (py3.12) | ✅ repro 2/2 | ✅ | ✅ | ✅ | not run |
| 2026.8.0 (py3.12) | ✅ repro 2/2 | ✅ | ✅ | ✅ | ✅ repro |
| macOS / Windows | not probed (pure routing logic, no path handling) |
Control: the same requirements.txt with no Pipfile.lock beside it is vendored correctly (./.socket/vendor/pypi/<uuid>/six-….whl ; <markers> # socket-patch vendor: six==1.16.0).
First bad version
Not bisected. v4.0.0 can't vendor against the mock (vendor_fetch_unverifiable), so I couldn't compare it. The routing precedence predates the #503 / #572 changes.
Suspect code
crates/socket-patch-core/src/vendor/pypi.rs:248detect_pypi_flavor: aPipfile.lock(likewisepoetry.lock/pdm.lock, untested here) wins without countingrequirements.txtas a competing install source (:286is the only place it's added topresent).crates/socket-patch-cli/src/commands/vendor.rs:2527: the takeover reverts the redirect in every file it touched, before the flavor router decides which single file to vendor.
Related, but different: #567 (hosted, -r include), #333 (hosted, closed), #328 / #503 (vendored → hosted takeover, which does rewire both files).
Backlog review — 2026-10-08
Priority: P1 → P2. The sibling requirements/Pipenv ambiguity is real, but #1044 added a warning and vendor/VEX checking rejects the mixed state. Retain the policy fix at P2.
Activity
mikolalysenko commentedon Oct 2, 2026
[agent] Triaged as priority:p1 (Pipenv). Not a duplicate of #567 (hosted
-rinclude path) or #504/#546 (venv discovery): this is the vendored flavor routerdetect_pypi_flavorreturning Pipenv without considering a sibling requirements.txt. No open PR addresses it.Generated by Claude Code
mikolalysenko commentedon Oct 3, 2026
[agent] uv bug-hunt routine (ledger #310): the same gap applies to uv.lock + a
uv exportrequirements.txt, the common Docker pattern.detect_pypi_flavor(crates/socket-patch-core/src/vendor/pypi.rs:296-325) adds only the standalone PEP 751 / script locks topresentwhenuv.lockexists.requirements.txtis never counted. Souv.lock+pylock.tomlcorrectly warnspypi_multiple_lockfiles("installs driven by pylock.toml will still install the UNPATCHED registry bytes"), butuv.lock+requirements.txtis silent. docs/testing/uv-compatibility.md also promises "When multiple applicable package-manager locks coexist, the CLI reports its precedence choice and the locks it leaves unchanged."Repro (Linux, main
045d7ec, real uv,mode_migration_pypi.rsfixtures for six 1.16.0):vendor --checkgreenuv pip sync/pip install -rvendor_unwiredvendor_unwiredvendor_unwiredvendor_unwiredHosted mode rewrites both files on the same project (
uv pip sync requirements.txtinstalls the patched wheel on 0.12.23), so only vendored has the gap. Same root cause, so I'm not filing it separately.Generated by Claude Code
uv: run 13 ledger (#723, #612 comment)
Test the pnpm-PnP memory route and the #612 takeover warning
mikolalysenko commentedon Oct 8, 2026
[agent] Pipenv bug-hunt re-triage (ledger #313) on main
b96a785, which includes #1044 (Refs #612). Linux, real Pipenv 2022.12.19 / 2023.12.1 / 2026.8.0 (py3.11), local mock patch API serving a patchedsix 1.16.0wheel,pipenv requirements > requirements.txt. Each cell ran on all three versions.Fixed by #1044: the silent parts.
scan --mode vendorednow emitspypi_multiple_lockfiles, which says requirements.txt "will still install the UNPATCHED" bytes. Exit 0.vendor --checknow fails with exit 1 (vendor_check_failed, "wiring contested … delete whichever of the two locks the project does not install from"). Before, it was green.Still open: the takeover. Starting from a hosted project where both files carry the hosted URL,
scan --mode vendoredreportsvendor_takeover_reverted_redirect. It turns requirements.txt back intosix==1.16.0from PyPI and vendors only Pipfile.lock. That run is now loud: it emitspypi_multiple_lockfiles, andvendor --checkexits 1. But a file still ends up less patched than the takeover found it. The #1044 commit says it leaves this as a separate mixed-mode policy question, so I'm leaving the issue open for that decision.Small leftover:
vexstill gives the remedy "rewire both locks (re-runsocket-patch scan/vendor) or delete the stale one". A re-run is a no-op (exit 0, nothing changes), so only the "delete the stale one" half works.vendor --check's wording is the accurate one.vendor --checkexit 1Generated by Claude Code
mikolalysenko commentedon Oct 8, 2026
[agent] Pipenv bug-hunt routine (ledger #313), main
823810a. Linux, real Pipenv 2023.12.1 and 2026.8.0 (py3.11), local mock patch API. Each cell ran twice.New variant: the requirements.txt pins the package through an
-rinclude. The project hasrequirements.txt=-r req/base.txt, andreq/base.txtispipenv requirementsoutput that pinssix==1.16.0.scan --mode vendoredwires only Pipfile.lock and exits 0. Unlike the plain-sibling shape, it emits nopypi_multiple_lockfileswarning, so the Decide which lockfile governs installs in one table #1044 scan-time warning doesn't follow-rincludes.vendor --checkdoes catch it: exit 1, "wiring contested: Pipfile.lock wires … but req/base.txt resolves the same version from elsewhere". So the CI gate is correct, and only the warning at scan time is missing.pypi_multiple_lockfilesat scanvendor --checkrequirements.txtrequirements.txt→-r req/base.txtTakeover (#1039), for context. On both shapes, the vendored → hosted takeover now commits as one unit, and
--dry-runwrites nothing.redirect_requirements_entry_not_found. That's Hosted scan in a Pipenv project whose requirements.txt pins the package through an-rinclude rewires only Pipfile.lock, then reports success while vex and rollback refuse the contested lock it just created #567.Generated by Claude Code
Read requirements files the way pip decodes them
mikolalysenko commentedon Oct 9, 2026
[agent] Pipenv bug-hunt routine (ledger #313), main
7a3c03a, which includes #1193 (the #1122 / #912 fix). Linux, real Pipenv 2026.8.0 (py3.11), local mock patch API serving a patchedsix 1.16.0wheel.New lane for this issue: the sibling lock is the
pylock.tomlthat Pipenv itself writes. With[pipenv] use_pylock = true,pipenv lockwrites bothPipfile.lockandpylock.toml. Since #1193,scan --mode vendoredwires onlyPipfile.lock, which fixes #1122's installs:pipenv syncand a freshpipenv install --deployboth get the PATCHED bytes. The project is still left in this issue's state:scan --mode vendored:status: success, exit 0,pypi_multiple_lockfiles("installs driven by pylock.toml will still install the UNPATCHED registry bytes").pylock.tomlhas no Socket reference.vendor --check: exit 1, "wiring contested … delete whichever of the two locks the project does not install from (re-vendoring changes nothing while both resolve it)".vex: exit 1, omits six (patched_ref_unattributable→vendor_unwired). Its remedy says "rewire both locks (re-runsocket-patch scan/vendor)", but a re-run givesalready_vendoredand leavespylock.tomluntouched, so the check stays at 1. That's the same remedy loop as the requirements.txt lane.pylock.toml) works until the nextpipenv lock, which regenerates it whileuse_pylock = trueis set.Pipfile.lockandpylock.toml,pipenv syncPATCHED, andvexattestsnot_affected.So on this documented layout (docs/testing/pipenv-compatibility.md, pylock row: "With a
Pipfile.lock, it is the file wired; the pylock is named inpypi_multiple_lockfiles"), vendored mode can't produce a greenvendor --checkor an attestation, while hosted can.I reproduced it 2/2: once with six in
[packages], once with six in[dev-packages](install --dev --deploy). Suspect:crates/socket-patch-core/src/vendor/pypi.rs(the #1193 router branch wiresPipfile.lockand only warns about the Pipenv-written pylock), while the check / vex contested rule treats that pylock as an independent install source. Either wiring both locks (as hosted does) or a remedy that doesn't loop would close this lane.Generated by Claude Code
4 remaining items
mikolalysenko commentedon Oct 9, 2026
v5 release blocker (P1). A normal Pipenv project can install through its exported requirements.txt. Vendoring/takeover must keep that declared install path patched too.
This follows the maintainer's release scope: one normally completing CLI instance, prioritizing valid-lockfile patch/install behavior, compatibility, and actionable CLI UX.
mikolalysenko commentedon Oct 9, 2026
[agent] Claiming for v5 blocker burn-down (shared root cause: vendored PyPI flavor routing wires only Pipfile.lock and never rewires a sibling exported requirements.txt). Branch: agent/v5-pipenv-vendor-sibling-reqs. Claim-ID: 2026-10-09T16:41:34Z-55133a
Vendor Pipenv's exported requirements.txt too
Test that co-wired Pipenv files are not contested
mikolalysenko commentedon Oct 9, 2026
[agent] Took over the stale claim on #1309 (Claim-ID 2026-10-09T18:21:01Z-7c41e9). First slice pushed: a fresh vendored Pipenv run now wires the exported
requirements.txtalong withPipfile.lock, revert restores both byte for byte, and discovery no longer reports the two files as contesting each other. Still to do (listed in the PR): the takeover e2e, re-running over projects vendored before this change, the superseding path, and the uv/pylock lanes.Generated by Claude Code
WIP: Fix vendored Pipenv sibling requirements.txt (#612)
Wire Pipenv's exported requirements in vendor
Pin the both-files takeover contract (#612)
Merge the parallel #612 attempt into this branch
Merge origin/main into agent/v5-pipenv-vendor-sibling-reqs
Harden the in-sync Pipenv export wiring and its revert (#612)
Vendor uv's exported requirements.txt too