Repository navigation
Fix pylock wiring ignoring an out-of-root -r pin (#1851) - #1853
Mikola Lysenko (mikolalysenko) wants to merge 7 commits into
Conversation
Assisted-by: Claude Code:claude-opus-5-5
A requirements.txt that pins a package only through a -r include outside the project root (-r ../shared/base.txt) was named a pypi_multiple_lockfiles loser next to a pylock, uv.lock, Pipfile.lock or other tool lock. The lock was wired, the run exited 0, and `pip install -r requirements.txt` kept installing the upstream release. Vendor can't edit that include, so it now refuses the package with pypi_requirements_outside_root, the same code the requirements-only path already used for this pin. Refs #1851 Assisted-by: Claude Code:claude-opus-5-5
`vendor --check` and `vex` read only the root requirements.txt and its in-root -r includes. A pin in an include outside the project root (-r ../shared/base.txt) was never seen, so a vendored pylock that wired the same release passed `vendor --check`, and `vex` attested not_affected, while `pip install -r requirements.txt` installed the upstream release. Discovery now reads the out-of-root includes the root reaches. Their registry pins and editable checkouts count as evidence against a lock's wiring, the same as an in-root pin. Nothing in them is read as wiring, because neither writer edits them. Refs #1851 Assisted-by: Claude Code:claude-opus-5-5
Review follow-up for #1851. The refusal and the contest only matched a clean `==` pin read as strict UTF-8. pip also installs the release from these lines in an out-of-root include: - an extras pin, `===`, or a pin with per-requirement options - a direct reference, or an editable checkout - a UTF-16 `pip freeze` export vendor wired the lock beside any of them, and only an `==` pin contested it afterwards. The planner and discovery now decode these files the way pip does and count every one of these shapes. An include that can't be read or decoded is now reported as unreadable instead of dropped silently. editable_location and egg_name move into utils::requirements so both sides parse lines with the same grammar. Refs #1851 Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
Bugbot follow-up for #1851. A bare archive URL, VCS URL (git+https://...#egg=six) or wheel path in an out-of-root include installs that artifact. Discovery already counted these lines as evidence, but the planner didn't refuse them, so `vendor` could still exit 0 and `vendor --check` would then fail. Both sides now read these lines through one helper, pypi_other::foreign_release. A line whose filename or #egg= names the package refuses: at the filename's version, or at any version when the line names none. Refs #1851 Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
|
[agent] CI on Generated by Claude Code |
|
[agent] On Generated by Claude Code |
|
Burn-down agent: labeled Ready for review at
Generated by Claude Code |
Final review briefWhat it does: Fixes #1851. Before, when a Risk: medium. It fails closed: there is no new write path and no new error code. The risk is the user-visible change. Projects that wired a lock beside such a pin used to pass and are now refused or flagged as contested. That is the intended fix, but users will notice it. Look here:
Verified: I read the full diff against #1851.
Changes I made: none. Open questions (non-blocking; edge cases, none a regression from main):
Auto-merge (squash) is armed, so approving sends this straight to the merge queue. Generated by Claude Code |
Final-review follow-up for #1851. If an out-of-root -r include existed but couldn't be read or decoded, the planner skipped it and wired the lock anyway, while discovery reported the same file as unreadable. pip may still install pins from that file, so vendor now refuses the package with pypi_requirements_outside_root and names the file. The vendor-dir check for direct references also normalizes Windows backslashes now, matching discovery. Refs #1851 Assisted-by: Claude Code:claude-opus-5-5
|
[agent] On the final review brief's open questions:
BugBot review Generated by Claude Code |
|
BugBot review Generated by Claude Code |
There was a problem hiding this comment.
✅ 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 70b2147. Configure here.
Final review brief (updated for
|
|
Burn-down agent: re-verified Generated by Claude Code |
LLM Description written by Claude Code:claude-opus-5-5
Fixes #1851
Summary
This covers a project whose
requirements.txtpins a package only through a-rinclude outside the project root (-r ../shared/base.txtholdingsix==1.16.0), next to apip lockpylock.tomlor another tool lock. Before this PR, vendored mode wired only the lock and exited 0.vendor --checkpassed, andvexattestednot_affected, whilepip install -r requirements.txtinstalled the upstream release.Now:
vendor/scan --mode vendored/ the hosted → vendored eject refuse the package withpypi_requirements_outside_root, which is the code the requirements-only path already uses for the same pin. They write nothing for it. This applies beside a pylock and beside a governing tool lock (uv, Poetry, PDM, Pipenv). Before, the tool-lock case didn't even name requirements.txt as a loser, because it only checked the root file.vendor --checkandvexsee a lock already wired beside such a pin (vendored before this fix, or a pin added later) as contested.vendor --checkexits 1 andvexomits the package, the same as for an in-root pin (CLI_CONTRACT.md "Contested locks").Root cause
Two code paths both skip a pin in an out-of-root include:
detect_pypi_flavor(vendor/pypi.rs) getssibling_pin_wirable == falsefor the read-only include, so it names requirements.txt apypi_multiple_lockfilesloser ("retain their existing sources") and wires the lock.extract_requirements(vex/discover/pypi_other.rs) reads onlyrequirements_include_names_in, which drops out-of-root includes. Nothing contests the lock's wiring.Change
pypi_requirements::pin_outside_rootfinds an out-of-root include through which pip installs the release from a non-vendored source:==/===pin (extras and per-requirement options allowed)#egg=names the packageIt decodes the file the way pip does, so a UTF-16
pip freezeexport counts too. The planner and discovery read these lines through the same helpers:utils::requirements::{editable_location, egg_name}andpypi_other::foreign_release.detect_pypi_flavorrefuses throughrefuse_pin_outside_rootin both lock branches. The refusal message is shared withplan_requirements(outside_root_refusal).pypi_requirements::requirements_outside_root_names_inlists the out-of-root includes the root reaches, through in-root files and through each other. Discovery reads them in the same install tree as the root file (so, as with Lockfile discovery treats a requirements.txt-rinclude as a competing lock, so after a hosted rewritevexattests nothing (exit 2) androllbackrefuses (exit 1), althoughpip install -r requirements.txtinstalls the patched wheel #1086, they never contest a wiring inside that tree). It reads them as evidence only: registry pins viaregistry_pin, editable checkouts, and non-Socket direct references and bare lines viaforeign_reference. An include that can't be read or decoded is reported aslockfile_unreadableinstead of being dropped silently. Nothing in them is read as wiring, because neither writer edits them. They share theMAX_REQUIREMENTS_FILESbudget.No wrapper changes:
npm/,pypi/andgem/only dispatch to the binary. No new error code.Tests (red → green)
../base.txtbeside pylock / uv.lock / Pipfile.lock / poetry.lock refuses; another release or an in-root-only package doesn'tvendor::pypi::tests::exact_pin_in_an_out_of_root_include_refuses_beside_a_lockunwrap_erronOk, pypi.rs:4179)vex::discover::pypi_locks::tests::out_of_root_include_pin_contests_a_vendored_pylock(also extras/===, direct reference, editable, bare VCS and path lines, and an undecodable include diagnosed)refsstill holds the pylock wiring)===,--config-settings, direct reference,-e/--editable=egg, baregit+…#egg=/ URL / path wheels, UTF-16 export; other-release and other-package bare wheels don't refusevendor::pypi::tests::every_installing_shape_in_an_out_of_root_include_refuses==in UTF-8; before 713eec9,Ok(PythonLocks)forgit+…#egg=six)vendorrefuses and leaves every file byte-identical; with another release in the include, the pylock vendors andvendor --checkpasses; once the include pins the vendored release,vendor --checkfails as contestedmode_migration_pypi::pip_lock_beside_an_out_of_root_include_pin_refusesvendorexit 0)Commands run locally:
cargo fmt --all -- --check: cleancargo clippy --workspace --all-features -- -D warnings: cleancargo test -p socket-patch-core --all-features --lib: all pass except 4 known permission-denial tests that can't fail a write as root in this sandbox (copy_tree::relax_loop_must_not_traverse_symlinked_root,vlt_heal::an_unremovable_hidden_lock_keeps_every_store_entry,pypi_poetry::wire_write_failure_…,pypi_requirements::wire_failure_rolls_back_…). The other agent PRs see the same 4, and none of them touches this change.cargo test -p socket-patch-cli --all-featuresformode_migration_pypi(75),e2e_vex(45),e2e_vex_vendor(53),e2e_vex_redirect(53),in_process_vendor_pypi_takeover,scan_requirements_lock_only,scan_vendor_requirements_unwired,hosted_superseding_pypi,vendor_ledger_schema_e2eande2e_pypi: all pass.713eec9is green (ci-ok), including the real-pipe2e_vex_buildlegs. Its first attempt lost 3 jobs to Depot runners (no logs, 10-minute cutoff, Depot runners lost mid-job evicted 2 merge-queue entries in 27h #1660), and one re-run of those jobs passed.Per-issue checklist
-rinclude outside the project root is ignored when a pylock.toml exists, sovendor --checkpasses andvexattests not_affected whilepip install -r requirements.txtinstalls the unpatched release #1851scan --mode vendored/vendor/ eject no longer wire only the pylock and exit 0:exact_pin_in_an_out_of_root_include_refuses_beside_a_lock,every_installing_shape_in_an_out_of_root_include_refuses,pip_lock_beside_an_out_of_root_include_pin_refuses-rinclude outside the project root is ignored when a pylock.toml exists, sovendor --checkpasses andvexattests not_affected whilepip install -r requirements.txtinstalls the unpatched release #1851vendor --checkdoesn't pass a pylock wiring that an out-of-root pin contests:pip_lock_beside_an_out_of_root_include_pin_refuses-rinclude outside the project root is ignored when a pylock.toml exists, sovendor --checkpasses andvexattests not_affected whilepip install -r requirements.txtinstalls the unpatched release #1851vexdoesn't attestnot_affected(same discovery contest that omits in-root contested pins):out_of_root_include_pin_contests_a_vendored_pylock/code-review high
Fixed in the review follow-up commit:
===/ per-requirement-option pins, direct references, editables and UTF-16 files in an out-of-root include were missed by the planner, and direct references were also missed by discovery.wired_with_lockparameter is removed.Bugbot (fixed in 713eec9): bare URL / VCS / path lines in an out-of-root include didn't refuse in the planner, though discovery contested them. Both sides now read them through one helper,
pypi_other::foreign_release. Bugbot's re-review of 713eec9 found no new issues.Not changed:
requirements_pins_targetreads only the root file. That predates this PR and isn't Vendored pip: a pin in a-rinclude outside the project root is ignored when a pylock.toml exists, sovendor --checkpasses andvexattests not_affected whilepip install -r requirements.txtinstalls the unpatched release #1851. Discovery already contests it, since in-root includes are in the tree, sovendor --checkandvexstay honest. A single tree-wide "pins target" predicate is a reasonable follow-up.detect_pypi_flavor, and discovery walks it once more to find out-of-root includes. These are small text files read once per routed package or discovery run, so I kept the separate, readable helpers rather than reshapesibling_pin_wirable's andrequirements_include_names_in's return types.Notes for review
pip install -r, sovendorrefusing it on the next run is the honest answer.vendor --checkalso fails it now.detect_pypi_flavorbranch as Fix extras pin un-hosted beside a pylock (#1794) #1796 (extras pins). The two changes are independent: whichever lands second needs a trivial merge.https://claude.ai/code/session_011f9aeR3uZQzo93CSAMyih2
Note
Medium Risk
Changes PyPI vendoring routing and VEX discovery around requirements includes; incorrect matching could block legitimate vendoring or miss contested installs, but behavior is fail-closed and heavily regression-tested.
Overview
Fixes #1851: when
requirements.txtpulls an exact package pin from a-rinclude outside the project root (e.g.-r ../shared/base.txt) next to a pylock or other governing lock, vendored mode no longer wires only the lock and reports success whilepip install -rstill installs the upstream release.Planner (
vendor/ vendored scan / eject):detect_pypi_flavornow callsrefuse_pin_outside_root, which usespin_outside_rootto walk the requirements tree and refuse withpypi_requirements_outside_root(same code as the requirements-only path) when pip would install the target from that unreadable include—covering==/===pins, direct references, editables, bare URL/VCS/path lines, UTF-16 exports, and fail-closed behavior when an include exists but cannot be decoded.Discovery (
vex/vendor --check):requirements_outside_root_names_inplusoutside_root_linetreat those includes as evidence only (not wiring), so an already-wired lock beside a matching out-of-root pin is contested like an in-root pin. Shared parsing helpers (editable_location,egg_name,bare_line_release/foreign_release) keep planner refusal and discovery contest aligned.Adds CLI and core tests for refusal,
--checkcontest, and multiple requirement line shapes.Reviewed by Cursor Bugbot for commit 70b2147. Configure here.
Generated by Claude Code