Skip to content

Fix requirements.txt writers ignoring pip hash mode (#376) - #383

Merged
Mikola Lysenko (mikolalysenko) merged 9 commits into
mainfrom
agent/fix-requirements-hash-mode
Oct 1, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 9 commits into
mainfrom
agent/fix-requirements-hash-mode

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

LLM Description written by Claude Code:claude-opus-5-5

Fixes #376

Summary

Hosted and vendored scans no longer break pip install -r on a requirements.txt that has no hashes.

Scope: this PR originally also fixed #378 (setup writing an unhashed hook line into a hash-pinned requirements.txt). #277 removed the setup command, so that part was dropped when main was merged in at a2dedc5, and #378 was closed as no longer applicable.

Root cause

pip's hash-checking mode is all or nothing. It turns on for the whole install as soon as any requirement carries --hash, and then every requirement, transitive dependencies included, has to be ==-pinned and hashed. The hosted redirect (patch/redirect/requirements.rs) and the vendored writer (vendor/pypi_requirements.rs::vendor_line) always added --hash, without checking which mode the requirements set was in. In an unhashed file that switches the mode on, and pip then refuses every other line.

Fix

  • Shared check: utils::requirements::requires_hashes. It is true when any line has a --hash option (any algorithm) or the file sets --require-hashes. Comments and URL #sha256= fragments don't count.
  • Hosted: a hashed file keeps --hash on the patched line, as before. An unhashed file gets name @ <url>#sha256=<hex> instead. Checked by hand:
    • pip 24 and uv 0.8 both verify the fragment (a wrong hash fails the install);
    • the fragment does not turn on pip's hash mode for other lines;
    • it also satisfies hash mode when that mode is on;
    • VEX already reads it (integrity_of).
  • Vendored: the mode is taken from the whole requirements tree (the root file plus its -r includes). A hashed tree keeps the vendor line unchanged. An unhashed tree gets the committed wheel path with no --hash, because pip can't read a fragment on a bare path. The inventory, VEX and the in-sync ledger fallback already accept a hashless vendor line.
    • Rebuild guard: when there is no ledger entry, the in-sync rebuild guard falls back to the pin on the wired line. A hashless line now still pins its wheel path (wired_pin_in returns an empty sha256 and pin_matches checks the path only). A rebuild under another filename is refused instead of leaving the line pointing at a missing file. Bugbot found this; it is fixed in fcc4fcc.
  • Re-scans are no-ops: every line counts toward the mode, the writer's own lines included, so a re-scan never flips a file between the two shapes. A file that an older version already wrote with --hash into an unhashed set keeps that shape; running rollback followed by a re-scan rewrites it in the new form.

Checklist

  • Hosted: redirect::requirements::tests::unhashed_file_pins_the_artifact_by_url_fragment_not_hash_option and hashed_file_keeps_the_hash_option. Both were red before the fix and are green after.
  • Vendored: vendor::pypi_requirements::tests::unhashed_requirements_get_an_unhashed_vendor_line (red → green) and hashes_in_an_include_keep_the_vendor_line_hashed.
  • Vendored rebuild guard: vendor::pypi::tests::in_sync_ledgerless_rebuild_of_unhashed_line_keeps_the_wired_path (red → green).
  • Real pip: a new unhashed cell (six==1.16.0 + idna==3.7) in e2e_vex_build::pip. Locally on pip 24.3.1 every hosted and vendored step passes: wire, install-patched, manifest-less VEX, re-scan.
  • Shared check: utils::requirements::tests::requires_hashes_reads_pip_hash_checking_mode.

Test updates (intended behavior change)

Several existing tests expected --hash in unhashed files, which is the #376 bug itself. They now expect the unhashed forms:

  • the unit tests in the hosted and vendored writers;
  • the redirect fixture and the VEX discovery golden (redirect-pypi.json);
  • in_process_get_hosted_ecosystems, e2e_hosted_production, e2e_vendored_production and e2e_vendor_pypi_build.

Two tests needed more than a new expected string:

  • vendor_ledger_schema_e2e: the base-binary parity test now names this one intended difference. The legacy fixtures are unchanged, so the legacy-revert coverage stays.
  • The pin-guard tests and the marker e2e rely on a hashed pin, so their inputs are now hash-pinned.

An earlier commit on this branch accidentally reformatted about 55 unrelated files, and e0ed1aa reverts them. main isn't rustfmt-clean and CI doesn't check formatting, so this PR leaves formatting alone.

Evidence

  • cargo clippy --workspace --all-features -- -D warnings: clean on fcc4fcc.
  • cargo test --workspace --all-features (local, sandbox runs as root): every failure is a test that needs a write, removal or permission check to fail, which root bypasses. None exercises code this PR changes.
  • SOCKET_PATCH_PIP_E2E_VERSIONS=24 SOCKET_PATCH_PIP_E2E_REQUIRED=1 cargo test -p socket-patch-cli --all-features --test e2e_vex_build -- pip:: --ignored: passes for all 4 cells × 2 modes (pre-merge).
  • CI is green on fcc4fcc, and Bugbot found no new issues on it.
  • Not touched: the npm, pypi and gem wrappers (they only dispatch to the binary).

🤖 Generated with Claude Code

https://claude.ai/code/session_017aQf44e9818AbFKDYnuAHZ


Note

Medium Risk
Changes how hosted and vendored requirements.txt pins are written, which affects install behavior for mixed hashed/unhashed projects; behavior is heavily tested but wrong detection could weaken or break installs.

Overview
Fixes pip breaking when requirements.txt is patched in an unhashed tree (#376). Hosted and vendored rewrites no longer unconditionally add --hash, which turns on pip’s global hash-checking mode and rejects every other requirement.

A shared requires_hashes helper detects whether the file (or included tree) is already in hash-checking mode. Hosted redirects pin integrity with a URL #sha256= fragment when the file is unhashed, and keep --hash when it is already hashed. Vendored wheel lines omit --hash in unhashed trees (path-only pin); the in-sync rebuild guard treats an empty sha256 as path-only via pin_matches.

CLI contract, redirect/VEX fixtures, and e2e tests (including a new pip VEX unhashed cell) are updated to match the new shapes.

Reviewed by Cursor Bugbot for commit fcc4fcc. Configure here.

Assisted-by: Claude Code:claude-opus-5-5
A hosted or vendored scan added --hash to the patched line of a
requirements file that had no hashes. pip then turns on hash-checking
mode for the whole install, so every other requirement and every
transitive dependency failed to install (#376).

Both writers now check whether the requirements set is already in
hash-checking mode. If it is, they keep writing --hash as before. If
not, hosted pins the patched wheel with the url's #sha256= fragment,
which pip still verifies, and vendored writes the committed wheel path
without a hash.

Assisted-by: Claude Code:claude-opus-5-5
socket-patch setup appended an unpinned, unhashed socket-patch[hook]
line to requirements.txt even when the file (or an -r include) was in
pip's hash-checking mode. pip then refused to install anything from it,
while setup reported success (#378).

setup now reports an error and leaves such a file untouched. setup
--remove also drops a hook line together with its backslash
continuation lines, so no stray --hash line is left behind.

Assisted-by: Claude Code:claude-opus-5-5
Adds an 'unhashed' cell (six plus idna, no hashes) to the real-pip
capstone. It asserts the wiring adds no --hash and that pip installs
the patched six. Also updates the redirect fixture to the #sha256=
url form written for unhashed files.

Assisted-by: Claude Code:claude-opus-5-5
The production and vendored e2e legs write a one-line unhashed
requirements.txt and then asserted a --hash pin, which is the #376
behavior itself. They now assert that no --hash is added. The hosted
leg also asserts the #sha256= url pin, which pip and uv both verify.

Assisted-by: Claude Code:claude-opus-5-5
- The VEX discovery golden now shows the #sha256= url on the hosted
  requirements ref.
- The hosted get test expects the url-fragment pin.
- The vendor ledger parity test allows the one intended change from
  the base binary: no --hash in an unhashed requirements.txt.
- The marker e2e installs with --require-hashes, so its input is now
  hash-pinned the way pip-compile writes it.

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review September 30, 2026 22:17
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

An earlier cargo fmt --all run reformatted about 55 files that this
fix doesn't otherwise touch; main isn't rustfmt-clean and CI doesn't
check formatting. This restores those files to main so the PR diff
only holds the hash-mode change and its tests.

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Bring the pip hash-mode fix onto the v5 workflow from #277. Main removed
the setup command, so the #378 hook-dependency guard in
setup/pypi/edit.rs has no home and is dropped with the file. The #376
half still applies: hosted and vendored requirements.txt writers only
emit --hash when the tree is already in pip's hash-checking mode. The
hosted-get test docs keep main's "no ledger" wording with the fragment
pin, and CLI_CONTRACT.md's requirements rows now describe the
conditional --hash.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQoii5oP1pwcJh5mzzo1HU
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread crates/socket-patch-core/src/vendor/pypi_requirements.rs
A requirements vendor line written into an unhashed requirements set has
no --hash, so wired_pin_in returned no pin and a ledgerless in-sync
rebuild skipped the guard entirely, including the path check. A rebuilt
wheel at another filename would then leave the wired line pointing at a
file that does not exist.

wired_pin_in now pins the path of a hashless line with an empty sha256,
and the guard treats an empty pinned sha256 as path-only. A hash that is
present but malformed still pins nothing, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017aQf44e9818AbFKDYnuAHZ
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit fcc4fcc. Configure here.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 1, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[burn-down agent] Ready for review on fcc4fcc: mergeable, 0 commits behind main.


Generated by Claude Code

@mikolalysenko Mikola Lysenko (mikolalysenko) changed the title Fix requirements.txt writers ignoring pip hash mode (#376, #378) Fix requirements.txt writers ignoring pip hash mode (#376) Oct 1, 2026
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 2, 2026
Since #383, a hosted scan of a requirements.txt that is not in
hash-checking mode pins the patched wheel with the url's #sha256=
fragment rather than --hash, so the PEP 440 regression test now
expects that form, matching the existing hosted pypi test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbFzTeYSTvB5iKjY6FRg7
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 2, 2026
* Start fix for #475

Assisted-by: Claude Code:claude-opus-5-5

* Match requirements pins like pip does (PEP 440)

A hand-written pin such as `six==1.16` installs six 1.16.0, but the
hosted requirements.txt rewrite compared versions as raw strings and
skipped it, so `scan` exited 0 and the project stayed unpatched. The
vendored requirements writer and the Hatch rewriter refused the same
pins as "not pinned".

Add a small PEP 440 equality helper (zero-padded release segments,
leading zeros, case and pre/post/dev spellings) and use it for `==`
pins in all three writers. `===` keeps plain string equality, as PEP
440 defines it.

Fixes #475

Assisted-by: Claude Code:claude-opus-5-5

* Test hosted get over a PEP 440-equivalent pin

Mirrors the #475 repro end to end: `get <uuid> --mode hosted` over
`requests==2.31`, `==2.31.0.0` and `Requests==02.31.0` must redirect
the pin to the hosted wheel. Fails on main, passes with the fix.

Assisted-by: Claude Code:claude-opus-5-5

* Expect #sha256= pin in the PEP 440 hosted-get test

Since #383, a hosted scan of a requirements.txt that is not in
hash-checking mode pins the patched wheel with the url's #sha256=
fragment rather than --hash, so the PEP 440 regression test now
expects that form, matching the existing hosted pypi test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbFzTeYSTvB5iKjY6FRg7

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review

Projects

None yet

3 participants