Repository navigation
Fix templated pip pins ignored beside pylock (#1873) - #1874
Mikola Lysenko (mikolalysenko) wants to merge 6 commits into
Conversation
Assisted-by: Claude Code:claude-opus-5-5
pip expands ${NAME} environment variables on every requirements line
before parsing it, so `six==${SIXV}` with SIXV=1.16.0 installs exactly
six 1.16.0. socket-patch expanded them only in -r/-c include targets,
so vendored scans and VEX discovery read the templated line as no pin.
Beside a pylock.toml that meant vendor wired only the lock, left the
line on PyPI and exited 0; `vendor --check` then verified the wiring
and `vex` attested not_affected while `pip install -r` installed the
unpatched release. Without a lock, vendor refused the line as "not
pinned".
Requirement lines are now expanded from the process environment the
way pip does, in both the vendored planner and VEX discovery. A
templated exact pin is wired like a literal one, and revert restores
the templated line as written. An unset or empty variable stays
literal, as in pip, and pins nothing.
Fixes #1873
Assisted-by: Claude Code:claude-opus-5-5
Covers #1873 end to end through the CLI: vendor wires a `six==${VAR}` line together with the pylock.toml, `vendor --check` is green, revert restores the templated line, and a pylock-only wiring the templated pin contests fails `vendor --check`. Assisted-by: Claude Code:claude-opus-5-5
pip expands ${NAME} where it installs (a Docker ARG, a CI step), which
socket-patch can't see. Reading `six==${SIXV}` through this process's
environment lets `vendor --check` pass wherever SIXV is unset, and lets
vendor rewrite the template away for every other environment.
So a templated requirement now fails closed instead:
- vendor refuses it with pypi_requirement_env_templated and writes
nothing, in the root file or an include, also beside a pylock.toml
or tool lock instead of naming requirements.txt an unpatched loser;
- VEX discovery and `vendor --check` read it as possibly installing
any release of the package from PyPI, so it contests every wiring
of that package and VEX no longer attests it.
A lowercase ${...} stays literal in pip and is still no pin.
Fixes #1873
Assisted-by: Claude Code:claude-opus-5-5
Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
1 similar comment
|
BugBot review Generated by Claude Code |
|
Ready for review at
Slack announcement not sent this run (the Slack send tool was unavailable); a later run will retry. Generated by Claude Code |
refuse_templated_pin runs in two branches of detect_pypi_flavor, the
pylock branch and the governing-lock branch. Only the pylock branch had
a test: with the governing-lock call removed, every core and CLI test
still passed. Add a Pipfile.lock project with requirements.txt pinning
six==${SIXV}, which must refuse with pypi_requirement_env_templated.
It fails without that call.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011f9aeS41XjBLw8LQsUUTYU
|
bugbot run Final reviewer: I pushed The new test. Two non-blocking gaps the description could list. Both are outside #1873:
A wording mismatch. A I removed 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 d97f89c. Configure here.
|
Final reviewer: claim released. Generated by Claude Code |
|
Ready for review at
Generated by Claude Code |
Final review brief (
|
LLM Description written by Claude Code:claude-opus-5-5
Fixes #1873
Summary
A pip requirement written with an environment variable (
six==${SIXV})beside a
pip lockpylock.toml left six on PyPI. Vendor wired only thelock and exited 0,
vendor --checkpassed, andvexattestednot_affectedwhilepip install -r requirements.txtinstalled theunpatched release. Such a line now fails closed:
pypi_requirement_env_templatedand writesnothing. That applies in the root file or an include, and beside a
pylock / Pipfile.lock / uv.lock / poetry.lock instead of naming
requirements.txt an unpatched
pypi_multiple_lockfilesloser. Withouta lock it used to say the misleading
pypi_requirement_not_pinned.vendor --checkread it as possibly installing anyrelease of the package from PyPI, so it contests every wiring of that
package (
vendor --checkexits 1, VEX drops the attestation).Root cause
pip runs
expand_env_variableson every requirements line before parsingit. socket-patch only knew
${NAME}for-r/-cinclude targets, so theplanner (
scan_pins) and discovery (pypi_other::requirements_line)read
six==${SIXV}as no pin at all, and nothing contested thepylock-only wiring.
Design choice: refuse, don't expand
The first commit expanded the line from this process's environment.
/code-review highshowed why that's wrong: pip expands where itinstalls, often in a Docker
ARGor CI step this process never sees.So
vendor --checkwould pass wherever the variable is unset, and avendor rewrite would freeze one environment's value into the file. The
final fix is env-independent. A line naming the package through a
pip-expandable
${NAME}(pip's[A-Z0-9_]+grammar) is "unknownrelease": the planner refuses it and discovery records any-version
evidence (
resolved_elsewhere_any_version, as for editable installs).This is the issue's "at minimum" option, and it's the only one that's
sound when the check runs somewhere other than the install.
Code-review findings, each with how it was handled:
--require-hasheshidden in a variable: a templated hash on the pin is refused (tested). A file-level${VAR}that expands to--require-hashesis not detected. That's a pre-existing gap outside Vendored pip: asix==${SIXV}pin in requirements.txt (pip expands${VAR}on every line) doesn't contest a pylock.toml wiring, sovendor --checkpasses andvexattests not_affected whilepip install -rinstalls the unpatched release #1873.redirect_unconfirmed).Stringallocated per line: short-circuited on${.Tests (red → green)
vendor::pypi_requirements::tests::refuses_an_env_templated_pin(root, include,--hash=${..}, lowercase stays literal)vex::discover::pypi_other::tests::issue_1873_env_templated_pin_contests_wiringmode_migration_pypi::pip_lock_vendor_refuses_an_env_templated_requirements_pin: vendor refuses beside a pip-lock pylock with and without the var set; a pylock-only wiring the templated line contests failsvendor --checkThe "without fix" column is the same tests run with
has_env_referencestubbed tofalse.Local runs:
cargo fmt --all -- --check: ok.cargo clippy --workspace --all-features -- -D warnings: ok.cargo test -p socket-patch-core --all-features --lib: 6996 passed, 4 failed. All 4 are pre-existing read-only-directory tests (copy_tree,vlt_heal,pypi_poetry/pypi_requirementswrite-failure) that can't fail as root in this container. They're unrelated to this diff.cargo test -p socket-patch-cli --all-features --test mode_migration_pypi --test in_process_vendor: 147 + 75 passed.A final reviewer pushed
d97f89c, which addsgoverning_lock_refuses_an_env_templated_requirements_pinto cover the refusal's Pipfile.lock / governing-lock branch.Known gaps outside #1873, left for follow-up:
${PKG}==1.0,${SIX_REQ}) has no readable name, so it is neither refused nor counted as evidence.requirements.txtstill falls back to thepypi_multiple_lockfilesloser warning.${TOKEN}credential in the target's direct-reference URL is refused withpypi_requirement_env_templated. That's fail-closed, but the "release it installs is unknown" wording fits it loosely.Wrappers (
npm/,pypi/,gem/) only dispatch to the binary, so they need no change.https://claude.ai/code/session_011f9aePT5mRBMMA1kpcGZBU
Generated by Claude Code