Skip to content

Fix uv restore ignoring lock file filters (#1604, #1613) - #1614

Open
Mikola Lysenko (mikolalysenko) wants to merge 9 commits into
mainfrom
agent/fix-uv-restore-file-filters
Open

Mikola Lysenko (mikolalysenko) wants to merge 9 commits into
mainfrom
agent/fix-uv-restore-file-filters

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

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

Fixes #1604
Fixes #1613

Summary

Hosted uv rollback / remove rebuild a lock entry from PyPI's JSON
API, 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 (and git checkout -- <lock> advice), as
the contract already did for the global exclude-newer.

Root cause

The guard in patch/redirect/upstream/uv.rs that decides whether a
lock's file set can be re-derived only checked the exact
[options] exclude-newer key and no-binary* / no-build* keys in
the paired pyproject or script metadata. It missed:

Fix

  • uv.lock / script locks: any exclude-newer* option other than
    exclude-newer-package refuses the whole lock (this adds
    exclude-newer-span). exclude-newer-package refuses only the hits
    it names; false means no cutoff.
  • uv pip compile pylocks (created-by = "uv", with a header saying
    uv pip compile, or no header and no index): refuse a hit when
    • uv's header command line (global options may come before pip
      or between pip and compile) has a filter flag that reaches it
      (--no-build, --exclude-newer, or --only-binary /
      --no-binary / --exclude-newer-package naming it or :all:),
      or
    • the uv settings uv pip reads have such a filter. These come from
      the nearest uv.toml, else a pyproject's [tool.uv] /
      [tool.uv.pip], then the user-level uv.toml
      ($XDG_CONFIG_HOME/uv, ~/.config/uv or %APPDATA%\uv), then the
      first system-level one ($XDG_CONFIG_DIRS, else /etc/uv). They
      merge as uv merges them: lists add up, per-package cutoffs merge
      entry by entry, otherwise the nearer file wins, and then [pip] wins
      over the top level. --no-config skips all of them, and
      UV_NO_CONFIG skips the user and system files. --config-file
      refuses, because the lock doesn't record that path.
      no-build-isolation* is not a filter, or
    • the header doesn't record a uv pip compile command line at all
      (--no-header, or a header replaced by UV_CUSTOM_COMPILE_COMMAND),
      so its flags can't be ruled out.
  • uv export pylocks and pip lock pylocks are unchanged: uv export
    records every file, and pip lock has its own rule.
  • CLI_CONTRACT.md and the module docs now describe both refusals.

Choices and limits

  • I verified with uv 0.11.32 that the header records these flags
    verbatim, e.g.
    # uv pip compile req.in --only-binary six --format pylock.toml ...,
    and that uv.toml / [tool.uv.pip] / [tool.uv] filters change
    the recorded files. UV_NO_BUILD / UV_NO_BINARY_PACKAGE had no
    effect on uv pip compile there.
  • A pip-compiled pylock with no recorded command line is now refused even
    when it was compiled without filters.
  • The user-level and system-level config read is the one on the machine
    running socket-patch, which is a proxy for the machine that ran the
    compile. UV_CONFIG_FILE in the environment is not followed.
  • A pylock compiled with --no-build by uv older than 0.9.16 (which
    kept every file) is now refused. The pylock doesn't record the uv
    version, so refusing is the safe choice.
  • Not changed, outside this cluster (raised by /code-review): the
    uv.lock lane's existing metadata check reads only pyproject
    [tool.uv], not a sibling uv.toml. It also treats
    no-build-isolation* and package lists that don't name the hit as
    filters, 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 a uv.toml
    no-binary-package uv.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:

Issue Test Covers
#1604 uv_exclude_newer_package_refuses_only_its_package uv.lock and script-lock lanes. A cutoff on the package (alone, or beside idna = false) and exclude-newer-span refuse. A cutoff on another package, or urllib3 = false, round-trips byte for byte.
#1613 pip_compile_pylock_build_filters_refuse_the_filtered_entry, plus unit tests settings_files_merge_key_by_key_with_pip_over_the_top_level and global_uv_settings_reads_the_user_then_first_system_file Header flags --no-build, --only-binary (name and =:all:), --no-binary (name, comma list, case), --exclude-newer and --exclude-newer-package refuse. uv.toml top 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.toml overriding pyproject, an unparsable pyproject, --no-config, and uv export pylocks. --config-file, a missing header, # make lock and # uv run poe lock headers 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.
  • Without the fix (main's uv.rs), both tests fail with the pin
    Restored instead of refused. With it, the whole
    upstream_restore_golden suite passes (53/53).
  • cargo fmt --all -- --check and
    cargo clippy --workspace --all-features -- -D warnings are clean.
  • cargo test -p socket-patch-core --all-features: every integration
    suite 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_requirements
    write-failure tests).
  • cargo test -p socket-patch-cli --all-features: same root-only
    write-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 is
    the 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.
  • A full cargo test --workspace didn't fit in the container's disk
    allowance, so I ran it per package as above.
  • The npm/pypi/gem wrappers only dispatch to the binary, so they need no
    change.

https://claude.ai/code/session_01FW66bMcoJmYGQykhbfuA8u


Generated by Claude Code

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
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 16, 2026 21:20
@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.

Comment thread crates/socket-patch-core/src/patch/redirect/upstream/uv.rs
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
@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.

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

Copy link
Copy Markdown
Collaborator Author

Labeled Ready for review at head 827cac5.

  • CI: 35/35 checks green (19 skipped). No main-wide failures.
  • Mergeable against main, no conflicts. CHANGELOG.md is untouched.
  • Bugbot: reviewed 827cac5, no new issues. The one earlier thread (on uv.rs) is resolved.
  • What to look at: the restore guard in crates/socket-patch-core/src/patch/redirect/upstream/uv.rs, which now refuses rebuilding a lock entry when [options.exclude-newer-package] or a uv pip compile pylock's build filters could have dropped files, and the matching CLI_CONTRACT.md text.
  • Slack announcement could not be sent this run (the Slack send tool is not available); the next run will retry.

Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[agent] uv bug-hunt verification (ledger #310) of head 827cac5 against real uv on Linux, using the bughunt mock patch server (six 1.16.0 and colorama 0.4.4 patches).

Fixed by this PR (main 657fce2 fails each of these, the PR passes):

Cell uv main PR
#1604 uv.lock exclude-newer-package = { colorama = … } in pyproject / uv.toml / as Colorama 0.13.1 restore +sdist, exit 0 hosted_restore_refused, lock left wired
#1604 cutoff on an unrelated package (attrs) 0.13.1 byte-identical byte-identical (no over-refusal)
#1604 PEP 723 script # exclude-newer-package, rollback and remove 0.13.1 +sdist refused
#1613 uv pip compile pylock --no-build / --only-binary six / --only-binary=six / --no-binary six 0.11.32, 0.13.1 +sdist / +wheel refused
#1613 uv.toml [pip] only-binary, [tool.uv.pip] no-binary, uv --no-config pip compile --no-build 0.13.1 – refused
Plain compile, --no-binary attrs, --no-build-isolation, a uv export --no-build pylock 0.11.32, 0.13.1 – restores (no over-refusal)

Gaps left (each reproduced on uv 0.11.32 and 0.13.1, once with rollback and once with remove, both exit 0 and the restored pylock gains six's sdist):

  1. uv pip compile --no-header --no-build. pip_compiled recognises the lock by the missing index, but there's no command line to read, so compile_filter finds nothing. The lock does show the filter: no sibling (attrs) has an sdist either.
  2. UV_CUSTOM_COMPILE_COMMAND="make lock" uv pip compile --only-binary six. The header reads # make lock, so uv_header_command returns None and the flags are lost the same way.
  3. A user-level $XDG_CONFIG_HOME/uv/uv.toml with [pip] no-build = true or only-binary = ["six"]. uv applies it to uv pip compile (the original pylock has no six sdist), but pip_settings only walks up from the lock's directory. The same likely applies to /etc/uv/uv.toml (not tested).

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. UV_NO_BUILD=1 / UV_ONLY_BINARY=six turned out not to filter what uv pip compile records on 0.13.1 (the six sdist is kept), so env vars aren't a 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
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[agent] All three gaps from the ledger #310 verification are closed in 64e40c4:

  1. and 2. A uv pip compile pylock whose header doesn't record a uv pip compile command line is now refused. That covers --no-header, UV_CUSTOM_COMPILE_COMMAND="make lock", and a custom command that starts with uv such as uv run poe lock. I did not use the sibling-shape heuristic, because --only-binary six leaves no trace on the siblings.
  2. Pylock restores now also read the user-level uv.toml ($XDG_CONFIG_HOME/uv, ~/.config/uv or %APPDATA%\uv) and the first system-level one ($XDG_CONFIG_DIRS, else /etc/uv, or %PROGRAMDATA%\uv on Windows). They merge as uv merges them: lists add up across files, per-package cutoffs merge entry by entry, and [pip] wins over the top level. UV_NO_CONFIG skips them.

The golden test and two new unit tests cover these cases. The golden suite (53/53) and e2e_redirect_uv_build (9/9) pass.


Generated by Claude Code

@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.

Comment thread crates/socket-patch-core/src/patch/redirect/upstream/uv.rs Outdated
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
@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.

Comment thread crates/socket-patch-core/src/patch/redirect/upstream/uv.rs
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
@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.

✅ 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.

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Still Ready for review, now at head 7dae64f. The label was first applied at an earlier head.

  • CI: 54/54 checks on 7dae64f passed (35 succeeded, 19 skipped). No checks are failing on main.
  • Mergeable against main with no conflicts. CHANGELOG.md is not changed.
  • Bugbot reviewed 7dae64f and found nothing new. All 3 review threads are resolved.

Generated by Claude Code

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

2 participants