Repository navigation
Fix uv restore ignoring lock file filters (#1604, #1613) - #1614
Mikola Lysenko (mikolalysenko) wants to merge 9 commits into
Conversation
Assisted-by: Claude Code:claude-opus-5-5
Hosted uv rollback and remove rebuilt a lock entry from every PyPI file of the release, even when the lock only kept some of them: a per-package exclude-newer-package cutoff (#1604) or a uv pip compile pylock built with --no-build, --only-binary, --no-binary or an exclude-newer cutoff (#1613). The restored lock then differed from what uv wrote. These restores are now refused, like the global exclude-newer already was. Assisted-by: Claude Code:claude-opus-5-5
Narrow the new refusals to the packages a filter actually reaches: an exclude-newer-package cutoff on another package, or a uv pip compile flag or uv setting naming another package, still restores the lock byte for byte. Document both refusals in CLI_CONTRACT.md. Assisted-by: Claude Code:claude-opus-5-5
Assisted-by: Claude Code:claude-opus-5-5
Follow how uv pip compile actually reads settings when deciding whether a pylock's files were filtered: [pip] wins over the top level, --no-config reads nothing, --config-file can't be checked and refuses, an unparsable pyproject is skipped, and a cutoff of false means no cutoff. This avoids refusing restores uv would have recorded in full. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
uv's pylock header keeps global options where they were typed, as in `uv --no-config pip compile ...` or `uv pip --quiet compile ...`. Such locks were not recognised as uv pip compile output, so their build filters went unchecked and a restore re-added filtered files. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
|
Labeled Ready for review at head
Generated by Claude Code |
|
[agent] uv bug-hunt verification (ledger #310) of head Fixed by this PR (main
Gaps left (each reproduced on uv 0.11.32 and 0.13.1, once with
For 1 and 2, the safe option seems to be refusing a pip-compiled pylock whose command line isn't recorded, or one whose siblings show the filter. For 3, reading uv's user and system config the way uv does would cover it, or else document the gap. printf 'six==1.16.0\nattrs==24.2.0\n' > req.in
uv pip compile -q req.in --no-build --no-header --format pylock.toml -o pylock.toml --python-version 3.11 # gap 1
# gap 2: UV_CUSTOM_COMPILE_COMMAND="make lock" uv pip compile -q req.in --only-binary six --format pylock.toml -o pylock.toml --python-version 3.11
# gap 3: XDG_CONFIG_HOME=$PWD/xdg with xdg/uv/uv.toml = '[pip]\nno-build = true', then a flagless compile
cp pylock.toml orig.toml; uv venv -q && uv pip sync -q pylock.toml
socket-patch scan --mode hosted --yes; socket-patch rollback pkg:pypi/six@1.16.0 # exit 0
diff orig.toml pylock.toml # > sdist = { url = ".../six-1.16.0.tar.gz", … }Generated by Claude Code |
A uv pip compile pylock whose header is missing (--no-header) or was replaced by UV_CUSTOM_COMPILE_COMMAND does not record the flags that filtered its files, so restoring it now refuses. Pylock restores also read the user-level and system-level uv.toml, as uv pip compile does. Settings files merge the way uv merges them: lists add up and per-package cutoffs merge entry by entry. UV_NO_CONFIG skips the global files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FW66bMcoJmYGQykhbfuA8u
|
[agent] All three gaps from the ledger #310 verification are closed in 64e40c4:
The golden test and two new unit tests cover these cases. The golden suite (53/53) and Generated by Claude Code |
|
BugBot review Generated by Claude Code |
A FIFO or device planted at a user- or system-level uv.toml path could block the restore. Read through read_regular_to_string, the helper the other host-config reads use, and skip a non-regular file the way uv does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FW66bMcoJmYGQykhbfuA8u
|
BugBot review Generated by Claude Code |
On Windows, opening a directory fails with PermissionDenied rather than InvalidInput, so a directory at a user- or system-level uv.toml path refused the restore. Check that the path is a file first, as uv does, then read it through the FIFO-safe opener. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FW66bMcoJmYGQykhbfuA8u
|
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 7dae64f. Configure here.
|
Still Ready for review, now at head
Generated by Claude Code |
LLM Description written by Claude Code:claude-opus-5-5
Fixes #1604
Fixes #1613
Summary
Hosted uv
rollback/removerebuild a lock entry from PyPI's JSONAPI, which lists every file of the release. When the lock had only kept
some of those files, the restored lock gained files the user's settings
had excluded, and the commands still exited 0. They now refuse with
hosted_restore_refused(andgit checkout -- <lock>advice), asthe contract already did for the global
exclude-newer.Root cause
The guard in
patch/redirect/upstream/uv.rsthat decides whether alock's file set can be re-derived only checked the exact
[options] exclude-newerkey andno-binary*/no-build*keys inthe paired pyproject or script metadata. It missed:
exclude-newer-package: on uv 0.12+ the restored uv.lock gains an sdist the per-package cutoff had excluded, so the restore isn't the original lock #1604:[options.exclude-newer-package]. uv 0.12+ applies thatcutoff per file, in uv.lock and PEP 723 script locks.
uv pip compile --no-build/--only-binary/--no-binarypylock.toml re-adds the artifacts uv filtered out (uv 0.9.16+), so the restore isn't the original lock #1613: PEP 751 locks fromuv pip compile. Since uv 0.9.16 theserecord only the files that
--no-build/--only-binary/--no-binary/--exclude-newer[-package]let through. A pylockhas no
[options]and no paired metadata, so neither check couldfire.
Fix
exclude-newer*option other thanexclude-newer-packagerefuses the whole lock (this addsexclude-newer-span).exclude-newer-packagerefuses only the hitsit names;
falsemeans no cutoff.uv pip compilepylocks (created-by = "uv", with a header sayinguv pip compile, or no header and noindex): refuse a hit whenpipor between
pipandcompile) has a filter flag that reaches it(
--no-build,--exclude-newer, or--only-binary/--no-binary/--exclude-newer-packagenaming it or:all:),or
uv pipreads have such a filter. These come fromthe nearest
uv.toml, else a pyproject's[tool.uv]/[tool.uv.pip], then the user-leveluv.toml(
$XDG_CONFIG_HOME/uv,~/.config/uvor%APPDATA%\uv), then thefirst system-level one (
$XDG_CONFIG_DIRS, else/etc/uv). Theymerge as uv merges them: lists add up, per-package cutoffs merge
entry by entry, otherwise the nearer file wins, and then
[pip]winsover the top level.
--no-configskips all of them, andUV_NO_CONFIGskips the user and system files.--config-filerefuses, because the lock doesn't record that path.
no-build-isolation*is not a filter, oruv pip compilecommand line at all(
--no-header, or a header replaced byUV_CUSTOM_COMPILE_COMMAND),so its flags can't be ruled out.
uv exportpylocks andpip lockpylocks are unchanged: uv exportrecords every file, and pip lock has its own rule.
Choices and limits
verbatim, e.g.
# uv pip compile req.in --only-binary six --format pylock.toml ...,and that
uv.toml/[tool.uv.pip]/[tool.uv]filters changethe recorded files.
UV_NO_BUILD/UV_NO_BINARY_PACKAGEhad noeffect on
uv pip compilethere.when it was compiled without filters.
running socket-patch, which is a proxy for the machine that ran the
compile.
UV_CONFIG_FILEin the environment is not followed.--no-buildby uv older than 0.9.16 (whichkept every file) is now refused. The pylock doesn't record the uv
version, so refusing is the safe choice.
uv.lock lane's existing metadata check reads only pyproject
[tool.uv], not a siblinguv.toml. It also treatsno-build-isolation*and package lists that don't name the hit asfilters, which gives false refusals. Hosted uv rollback / remove ignore
exclude-newer-package: on uv 0.12+ the restored uv.lock gains an sdist the per-package cutoff had excluded, so the restore isn't the original lock #1604 notes that auv.tomlno-binary-packageuv.lock restore was still refused in practice.Worth a follow-up issue.
Tests (red → green)
New tests in
crates/socket-patch-core/tests/upstream_restore_golden.rs:uv_exclude_newer_package_refuses_only_its_packageidna = false) andexclude-newer-spanrefuse. A cutoff on another package, orurllib3 = false, round-trips byte for byte.pip_compile_pylock_build_filters_refuse_the_filtered_entry, plus unit testssettings_files_merge_key_by_key_with_pip_over_the_top_levelandglobal_uv_settings_reads_the_user_then_first_system_file--no-build,--only-binary(name and=:all:),--no-binary(name, comma list, case),--exclude-newerand--exclude-newer-packagerefuse.uv.tomltop level and[pip], and pyproject[tool.uv]/[tool.uv.pip], refuse. These round-trip: filters naming other packages,:none:,--no-build-isolation,=false,[pip]overriding the top level,uv.tomloverriding pyproject, an unparsable pyproject,--no-config, anduv exportpylocks.--config-file, a missing header,# make lockand# uv run poe lockheaders refuse. The unit tests cover the user and system lookup (XDG, HOME, Windows, the empty-entry cut-off,UV_NO_CONFIG, invalid TOML) and the merge rules.uv.rs), both tests fail with the pinRestoredinstead of refused. With it, the wholeupstream_restore_goldensuite passes (53/53).cargo fmt --all -- --checkandcargo clippy --workspace --all-features -- -D warningsare clean.cargo test -p socket-patch-core --all-features: every integrationsuite passes. 4 lib tests fail locally only because the container runs
as root, so chmod-based write failures can't happen
(
copy_tree/vlt_heal/pypi_poetry/pypi_requirementswrite-failure tests).
cargo test -p socket-patch-cli --all-features: same root-onlywrite-failure tests fail (
in_process_redirect×3,repair×2).So does one bun rollback test, because the container's network blocks
the blob download (
missing_blob). None of these touch uv. CI isthe authority for all of them.
cargo test -p socket-patch-cli --all-features --test e2e_redirect_uv_build -- --ignored(uv 0.11.32): 9/9 pass.
cargo test --workspacedidn't fit in the container's diskallowance, so I ran it per package as above.
change.
https://claude.ai/code/session_01FW66bMcoJmYGQykhbfuA8u
Generated by Claude Code