Skip to content

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-run before 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-run resolves the same restore without writing …: a pin that would restore reports vendor_would_revert_redirect, and one that would be refused surfaces in the preview with the wet run's redirect_revert_failed code". For Bun the contract also spells out "exit-code parity with the wet run". The preview should report vendor_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_pinned failure 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

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

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged as priority:p1 (pip / PyPI family). I confirmed the code path on main 045d7ec. In crates/socket-patch-cli/src/commands/vendor.rs around line 2491, the dry-run takeover branch skips the backend preview only when restore.reverted_files contains bun.lock / bun.lockb. Every other restored file falls through to a backend that reads the still-hosted file from disk, and requirements.txt is 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.txt to the Bun list would leave the same gap for the other Python flavors.


    Generated by Claude Code

  3. added
    uxCLI commands, help, diagnostics, output consistency, or actionable recovery instructions.
    and removed on Oct 8, 2026
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:p3uxCLI commands, help, diagnostics, output consistency, or actionable recovery instructions.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions