Repository navigation
vendor --dry-run over a hosted requirements.txt pin previews a pypi_requirement_not_pinned failure (exit 1), but the real vendor takes it over and succeeds #668
Description
[agent] Found by the scheduled pip / requirements.txt bug-hunt routine (ledger #309).
Summary
On a project whose requirements.txt was rewritten by hosted mode (six @ https://…/six-1.16.0-…whl#sha256=…), socket-patch vendor correctly does the hosted → vendored takeover: it restores six==1.16.0, vendors the wheel, and exits 0. But socket-patch vendor --dry-run on the same project prints the vendor_would_revert_redirect advisory and then also a failed event pypi_requirement_not_pinned ("requirements.txt: six is not pinned to ==1.16.0; pin it exactly or use agent mode …"). It exits 1 / partialFailure.
The preview runs the requirements.txt backend against the on-disk file, where the hosted six @ <url> line is still live, so the pin check refuses a line the wet run never sees. This is the same failure mode vendor.rs already special-cases for Bun (the comment at the bun.lock / bun.lockb check explains it), but not for requirements.txt.
Impact
- A user (or CI gate) who runs
vendor --dry-runbefore switching a hosted pip project to vendored is told it will fail, with advice to "pin it exactly or use agent mode", even though the pin is exact and the real run works. Pipelines that gate on the dry run's exit code block a supported migration. - The preview contradicts itself: it says both "a non-dry-run vendor will restore … then vendor" and "failed".
Repro
Uses the repo's own fixtures: mode_migration_pypi.rs (hosted wiremock API) plus prebuilt_common::mount_project on the same mock server for the vendored artifact. Throwaway test body:
std::fs::write(root.join("requirements.txt"), "idna==3.7\nsix==1.16.0\n").unwrap();
let server = MockServer::start().await;
mount_hosted_api(&server, true).await;
hosted_scan(&root, &server); // six -> six @ <hosted url>#sha256=…
stage_manifest(&root);
prebuilt_common::mount_project(&server, &root).await;
let uri = server.uri();
// preview
run_cli(&root, &["vendor", "--dry-run", "--vendor-url", &uri, "--patch-server-url", &uri], &[]);
// wet run
run_cli(&root, &["vendor", "--vendor-url", &uri, "--patch-server-url", &uri], &[("NO_PROXY","localhost,127.0.0.1")]);vendor --dry-run (exit 1, requirements.txt unchanged):
[{"action":"skipped","purl":"pkg:pypi/six@1.16.0","errorCode":"vendor_would_revert_redirect",
"reason":"pkg:pypi/six@1.16.0 is hosted; a non-dry-run vendor will restore its upstream registry entry first, then vendor (mode takeover)"},
{"action":"failed","purl":"pkg:pypi/six@1.16.0","errorCode":"pypi_requirement_not_pinned",
"error":"requirements.txt: six is not pinned to ==1.16.0; pin it exactly or use agent mode (`scan --mode agent` + `socket-patch apply`) instead"}]vendor (exit 0, status: success):
[{"action":"skipped","errorCode":"vendor_takeover_reverted_redirect", "...": "restored its upstream registry entry (requirements.txt) before vendoring (mode takeover)"},
{"action":"applied","purl":"pkg:pypi/six@1.16.0","files":[{"path":"six.py","verified":true}]},
{"action":"skipped","errorCode":"vendor_prebuilt_downloaded"}]The resulting requirements.txt (./.socket/vendor/pypi/<uuid>/six-1.16.0-py3-none-any.whl # socket-patch vendor: six==1.16.0) installs the patched six with real pip install -r requirements.txt on pip 26.2.1 / CPython 3.13 and pip 20.3.4 / CPython 3.8.
Expected vs actual
- Expected (crates/socket-patch-cli/CLI_CONTRACT.md, "Takeover reconciliation"): "
--dry-runresolves the same restore without writing …: a pin that would restore reportsvendor_would_revert_redirect, and one that would be refused surfaces in the preview with the wet run'sredirect_revert_failedcode". For Bun the contract also spells out "exit-code parity with the wet run". The preview should reportvendor_would_revert_redirect(plus whatever the backend would do with the restored line) and exit 0, as the wet run does. - Actual: the preview adds a
pypi_requirement_not_pinnedfailure the wet run never produces, and exits 1.
Matrix (Linux, main 045d7ec)
| requirements.txt before hosting | vendor --dry-run |
vendor |
|---|---|---|
idna==3.7 + six==1.16.0 |
fail pypi_requirement_not_pinned, exit 1 (reproduced twice) |
pass, exit 0 |
hashed (six==1.16.0 --hash=…, idna==3.7 --hash=…) |
fail pypi_requirement_not_pinned, exit 1 |
pass, exit 0 |
six==1.16.0 alone |
redirect_revert_failed in both (consistent; the sole-pin refusal is #410) |
same |
scan --mode vendored --dry-run over the same project previews would_vendor correctly; only the vendor command's preview is affected. OS-independent: it's a code-path issue in the dry-run branch, not a filesystem one. I haven't checked the other Python flavors (Poetry / Pipenv / uv / Hatch), whose hosted rewrites also replace the version spec. They likely hit the same path and are owned by sibling routines.
Suspect code
crates/socket-patch-cli/src/commands/vendor.rs:2493-2517: under common.dry_run, after the vendor_would_revert_redirect advisory, only bun.lock / bun.lockb skips the backend preview ("The backend preview below reads the lock from disk, where the hosted wiring is still live…"). requirements.txt falls through to the backend, which refuses the hosted line at crates/socket-patch-core/src/vendor/pypi_requirements.rs:476 (pypi_requirement_not_pinned).
No probe runs: this is OS-independent and was reproduced on Linux only.
Backlog review — 2026-10-08
Priority: P1 → P3. Dry-run predicts a failure for a takeover that the real command performs successfully. Preview accuracy is lower priority than patch correctness.
Activity
pip: run 2026-10-03T08:23Z (#668)
mikolalysenko commentedon Oct 3, 2026
[agent] Triaged as
priority:p1(pip / PyPI family). I confirmed the code path on main045d7ec. Incrates/socket-patch-cli/src/commands/vendor.rsaround line 2491, the dry-run takeover branch skips the backend preview only whenrestore.reverted_filescontainsbun.lock/bun.lockb. Every other restored file falls through to a backend that reads the still-hosted file from disk, andrequirements.txtis one of them. I didn't find a duplicate or an existing fix PR.The fix belongs at that boundary. When a dry run plans to restore a hosted entry, the preview has to either run the backend against the restored content or stop after the advisory for every flavor whose hosted rewrite replaces the version spec. Adding
requirements.txtto the Bun list would leave the same gap for the other Python flavors.Generated by Claude Code