Skip to content

Fix uv pylock wiring wheels under no-binary (#1603) - #1672

Merged
Mikola Lysenko (mikolalysenko) merged 7 commits into
mainfrom
agent/fix-uv-pylock-no-binary-wire-guard
Oct 23, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 7 commits into
mainfrom
agent/fix-uv-pylock-no-binary-wire-guard

Conversation

@mikolalysenko

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

Copy link
Copy Markdown
Collaborator

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

Fixes #1603

This finishes #1603. #1649 fixed the uv project, uv.toml and PEP 723 script lanes and said Refs, leaving the PEP 751 lane from the issue's follow-up comment. That lane was waiting on #1614, which has now merged.

Root cause

uv installs a package only from an sdist when it falls under --no-binary, and every Socket artifact is a wheel. uv pip compile --no-binary six --format pylock.toml records six with only its sdist. Both scan modes still replaced that entry with the patched wheel and exited 0. After that, the matching uv pip sync failed with "marked as --no-binary but is itself a binary distribution". Nothing at wire time read the two places this filter can come from:

  • the uv pip compile command line recorded in the lock header, and
  • uv's settings files.

#1614 already reads both of these, but only on the hosted restore side.

Fix

upstream::uv::pylock_no_binary is shared by both modes and refuses before any write. It only looks at a lock with created-by = "uv", and refuses when either:

  • the header's uv pip compile passes --no-binary naming the package or :all:. As in uv, :none: clears the values given before it.
  • a uv setting covers the package: [pip] no-binary, a top-level no-binary = true, or no-binary-package. This applies even when the header shows --no-config, because that flag only kept settings away from the compile and uv pip sync still reads them (Bugbot finding, fixed in db67fc0).

The settings it reads are the root's uv.toml, else pyproject's [tool.uv], else the nearest parent settings file. That is the same lookup #1501/#1545 use, carried by the existing UV_ANCESTOR_CONFIG_KEY.

Checked against real uv 0.11.32 with a lock that has only the wheel:

  • [pip] and top-level settings add up rather than one replacing the other.
  • no-binary = false turns off only the top-level package list.
  • :none: clears the earlier values.

What each mode does:

Pylocks that Pipenv installs (wire_pipenv_pylocks / pipenv_reads_pylock) are not checked, because Pipenv reads no uv setting. A user- or system-level uv.toml is not read, for the same reason #1649 gave: it belongs to the machine that installs. docs/testing/uv-compatibility.md is updated.

Tests (red → green)

Issue lane Test Without fix With fix
#1603 hosted pylock (header flag, :all:, [pip]/top-level/pyproject settings, parent settings; --no-config + settings; controls incl. :none:, Pipenv, non-uv lock) patch::redirect::python_lock_warning_tests::pylock_no_binary_refuses_a_wheel_uv_would_not_install FAILED ok
#1603 vendored pylock (same matrix, parent uv.toml, Pipenv path) vendor::pypi_lock::tests::pylock_no_binary_refuses_before_writing FAILED ok
#1603 CLI, both modes, writes nothing mode_migration_pypi::uv_pylock_no_binary_is_refused_in_both_modes FAILED (hosted pinned 1) ok

"Without fix" means the check was short-circuited to None.

Local runs (Linux, uv 0.11.32):

  • cargo fmt --all -- --check and cargo clippy --workspace --all-features -- -D warnings: clean.
  • cargo test -p socket-patch-core -p socket-patch-cli --all-features --lib: CLI 925 ok. Core 6641 ok and 4 failed. The 4 are permission-injection tests (copy_tree relax_loop, vlt_heal unremovable lock, poetry/requirements wire-write failure) that can't fail when run as root, which this sandbox is. This change doesn't touch them.
  • --test mode_migration_pypi: 68 ok. --test e2e_redirect_uv_build --include-ignored: 23 ok. --test e2e_vendor_pypi_build --include-ignored: 55 ok.
  • The full cargo test --workspace ran out of the sandbox's disk allowance while building every integration binary, so CI covers the rest.

CI follow-up

On a33ca5e, coverage failed in upstream_restore_golden::pip_compile_pylock_build_filters_refuse_the_filtered_entry. That test built its hosted lock by running the hosted rewrite on a --no-binary lock, which the rewrite now refuses. It now wires the plain lock first and then adds the flag or setting, which is how such a lock reaches the restore in real use. All 31 core integration binaries pass locally (0656163).

/code-review high

Fixed:

  • parent settings weren't read
  • the uv refusal shadowed Pipenv's
  • :none: was ignored
  • duplicated --no-binary parsing (now one binary_flag_covers, also used by the restore side)
  • settings were re-read per lock
  • basename vs full-path mismatch

Not changed:

  • Re-run of an already-vendored lock now refuses. That lock is already broken for uv pip sync, and the uv project lane from Fix uv scans wiring wheels under no-binary (#1603) #1649 behaves the same way (pypi_uv_no_binary is not supersedable).
  • One extra lock parse per dep. It's a small cost on the refusal path, and keeping the per-dep check self-contained is simpler.

https://claude.ai/code/session_015NdfJ6nfxCRsW1FVjrkuPm


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
A PEP 751 lock from `uv pip compile --no-binary six` records six with
only its sdist, and `uv pip` installs six only from an sdist under that
flag or a uv `no-binary` setting. Both scan modes still swapped in the
patched wheel and exited 0, so the matching `uv pip sync` then failed
("marked as `--no-binary` but is itself a binary distribution").

Hosted and vendored scans now refuse such a lock before writing
anything when the `uv pip compile` command line in its header passes
`--no-binary` naming the package or `:all:`, or the root's uv.toml /
`[tool.uv]` covers it through `[pip] no-binary`, `no-binary = true` or
`no-binary-package`. Checked against uv 0.11.32: `[pip]` and top-level
settings add up, and `no-binary = false` turns off only the top-level
package list. Locks written by other tools are left alone.

Fixes #1603

Assisted-by: Claude Code:claude-opus-5-5
Review follow-ups for the pylock no-binary refusal:

- uv takes `uv pip`'s settings from the nearest parent directory when
  the project root sets none, so a parent `uv.toml` or `[tool.uv]` with
  `no-binary` now refuses too, as it already does for uv projects and
  scripts.
- Pipenv installs the pylocks it reads and ignores uv settings, so
  those locks keep Pipenv's own refusal (hosted) or are wired as before
  (vendored) instead of getting a uv error.
- `--no-binary :none:` clears the earlier values, as in uv, so a lock
  compiled with `--no-binary :all: --no-binary :none:` is not refused.
  The restore side now reads the flag the same way.
- The root settings are read once per run instead of once per lock.

Refs #1603

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 22, 2026 21:25
@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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: --no-config skips install settings
    • Removed the early return that skipped project settings checks when --no-config was present, since uv pip sync still applies those settings even when compile used --no-config.

Create PR

Or push these changes by commenting:

@cursor push cecfabd11a
Preview (cecfabd11a)
diff --git a/crates/socket-patch-core/src/patch/redirect/mod.rs b/crates/socket-patch-core/src/patch/redirect/mod.rs
--- a/crates/socket-patch-core/src/patch/redirect/mod.rs
+++ b/crates/socket-patch-core/src/patch/redirect/mod.rs
@@ -26365,6 +26365,8 @@
             ),
             // `:none:` clears only the values before it.
             (" --no-binary :none: --no-binary six", &[]),
+            // `uv pip compile --no-config` skips settings, but sync still applies them.
+            (" --no-config", &[("uv.toml", "no-binary = true\n")]),
         ];
         for (flags, extra) in refusals {
             let result = run(flags, extra);
@@ -26393,10 +26395,8 @@
                 warning.detail
             );
         }
-        // Another package's flag or setting, no-build, a list turned off by
-        // no-binary = false, a uv.toml replacing pyproject's settings, and
-        // settings `uv pip compile --no-config` never read leave the wheel
-        // installable.
+        // Another package's flag or setting, no-build, or a list turned off by
+        // no-binary = false leave the wheel installable.
         let wired: &[(&str, &[(&str, &str)])] = &[
             ("", &[]),
             (" --no-binary idna", &[]),
@@ -26419,7 +26419,6 @@
                     ),
                 ],
             ),
-            (" --no-config", &[("uv.toml", "no-binary = true\n")]),
             (" --no-binary :all: --no-binary :none:", &[]),
             // The root's own settings stop the lookup above it.
             (

diff --git a/crates/socket-patch-core/src/patch/redirect/upstream/uv.rs b/crates/socket-patch-core/src/patch/redirect/upstream/uv.rs
--- a/crates/socket-patch-core/src/patch/redirect/upstream/uv.rs
+++ b/crates/socket-patch-core/src/patch/redirect/upstream/uv.rs
@@ -737,9 +737,6 @@
                 "the lock's `uv pip compile --no-binary` covers {name}, {consequence}"
             ));
         }
-        if command.contains(&"--no-config") {
-            return None;
-        }
     }
     let (path, key) = settings_no_binary(settings, &name)?;
     Some(format!(

diff --git a/crates/socket-patch-core/src/vendor/pypi_lock.rs b/crates/socket-patch-core/src/vendor/pypi_lock.rs
--- a/crates/socket-patch-core/src/vendor/pypi_lock.rs
+++ b/crates/socket-patch-core/src/vendor/pypi_lock.rs
@@ -1789,6 +1789,8 @@
                     pyproject("[tool.uv]\nno-binary-package = [\"two\"]\n"),
                 )),
             ),
+            // `uv pip compile --no-config` skips settings, but sync still applies them.
+            (" --no-config", Some(("uv.toml", "no-binary = true\n"))),
         ] {
             let Err(error) = load(pylock(flags), config).await else {
                 panic!("{flags:?} with {config:?} must refuse");
@@ -1797,10 +1799,8 @@
             assert!(error.1.starts_with("pylock.toml: "), "{}", error.1);
             assert!(error.1.contains("only from an sdist"), "{}", error.1);
         }
-        // Another package's flag or setting, no-build, a list turned off by
-        // no-binary = false, a uv.toml that replaces pyproject's settings,
-        // and settings `uv pip compile --no-config` never read leave the
-        // wheel installable.
+        // Another package's flag or setting, no-build, or a list turned off by
+        // no-binary = false leave the wheel installable.
         for (flags, config) in [
             ("", None),
             (" --no-binary six", None),
@@ -1813,7 +1813,6 @@
                     "no-binary = false\nno-binary-package = [\"two\"]\n",
                 )),
             ),
-            (" --no-config", Some(("uv.toml", "no-binary = true\n"))),
             (" --no-binary :all: --no-binary :none:", None),
         ] {
             if let Err(error) = load(pylock(flags), config).await {

You can send follow-ups to the cloud agent here.

Comment thread crates/socket-patch-core/src/patch/redirect/upstream/uv.rs Outdated
The uv pylock restore test made its hosted lock by running the hosted
rewrite on a lock already under `--no-binary`. That rewrite now refuses
such a lock, so the test wires the plain lock first and then adds the
flag or setting. That is how such a lock reaches the restore in real
use: it was wired before this check existed, or filtered later.

Refs #1603

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.

`uv pip compile --no-config` only keeps settings files away from the
compile. The later `uv pip sync` still reads uv.toml, so a `no-binary`
setting there still makes it refuse the patched wheel. The pylock
check now reads the settings whatever the header says.

Refs #1603

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 22, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Ready for review at db67fc0. CI: all 54 check runs settled green (35 success, 19 skipped); no main-wide failures. Bugbot reviewed db67fc0 with no new findings; its earlier finding on upstream/uv.rs was fixed and resolved. Reviewers should look at upstream::uv::pylock_no_binary (the :none: reset and --no-config handling). Slack announcement not sent: no Slack send tool available in this run.


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Final review brief

What it does. Before it writes a patched wheel into a uv-made PEP 751 pylock, socket-patch now checks whether uv would refuse to install that wheel, and refuses first (finishes #1603). It refuses in two cases:

  • the lock header's uv pip compile line passes --no-binary for the package or for :all: (a later :none: clears earlier values);
  • a uv setting covers the package ([pip] no-binary, top-level no-binary, no-binary-package), read from uv.toml, then [tool.uv], then the parent settings file.

Hosted mode marks the patch refused with redirect_uv_lock_unsupported, and vendored mode returns pypi_uv_no_binary. Either way nothing is written.

Risk: low. The change only adds refusals; it never writes anything new.

  • It fails open: a malformed TOML file or a missing header skips the check, and nothing panics.
  • Pipenv-installed pylocks and non-uv locks are left alone.
  • In vendored mode the check runs during the read-only lock load, before wire_python_locks.

Look here

  • upstream/uv.rs:719:`` pylock_no_binary, `binary_flag_covers` (:750) and `settings_no_binary` (:778).```` This is where the precedence and `:none:` rules live.
  • redirect/mod.rs:6236: the hosted check. It continues before any content change.
  • vendor/pypi_lock.rs:267: the vendored check. Settings are read once per load, not once per dependency.
  • hosted/engine.rs:714: the pylock lane now also reads the root uv.toml and the parent settings file.

Verified

  • I read the full diff (8 files). CHANGELOG.md is untouched, there is no debug code and nothing is out of scope.
  • Header parsing handles --no-binary=Foo, --no-binary a,b, PEP 503 name folding and CRLF headers.
  • Ran cargo test -p socket-patch-core --lib -- uv pylock: 250 passed, 0 failed.
  • Ran --test upstream_restore_golden -- pylock: 7 passed, including the reworked filter test.
  • The new tests assert a refusal and an unchanged tree for inputs the old code wired, so they fail without the fix.
  • CI on db67fc0: 35/35 green (19 skipped by path filters), ci-ok and clippy green. Bugbot ran on this head with no findings. Its one earlier thread is resolved.

Changes I made: none.

Open questions (not blocking)

  • A settings list no-binary = [":all:", ":none:"] still refuses (uv.rs:860 treats :all: as a match without applying the later :none:). Header flags do apply :none:. This errs toward refusing and is rare, so a follow-up is fine.
  • On a re-run, a project vendored before this change with a --no-binary lock now gets pypi_uv_no_binary instead of "in sync". This is intended (that lock already breaks uv pip sync), and the PR description covers it.

Auto-merge (squash) is armed, so approving sends it straight to the merge queue.


Generated by Claude Code

uv reads `[pip] no-binary = [":all:", ":none:"]` in order, the same
way it reads repeated `--no-binary` flags: `:none:` clears the values
before it, so `uv pip sync` installs the patched wheel (checked with uv
0.11.32). The pylock check treated any `:all:` in the list as covering
the package and refused a lock that would install fine. Both the list
and the header flags now go through one ordered reader.

Refs #1603

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

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[agent] Fixed the open question from the final review brief in 6ef706e. With uv 0.11.32, [pip] no-binary = [":all:", ":none:"] installs the patched wheel, [":none:", "six"] refuses it, and ["six", ":none:"] installs it. So a settings list is read in order, the same as repeated flags. The settings list and the header flags now share one ordered reader (specifiers_cover). The hosted test gained a refusal case and a control case. clippy and fmt are clean, and upstream_restore_golden (53) and mode_migration_pypi (68) pass locally. Auto-merge stays armed. This push re-runs CI and Bugbot.


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.

In a uv workspace member, `uv pip` reads the workspace root's settings
and never the member's own `[tool.uv]`, so a root-level no-binary
setting would break a member pylock that the no-binary check reads
from the member. Both modes already refuse a run in a governed member
before reading any lock (#1138), so that pylock is never wired. This
test pins that.

Refs #1603

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.

✅ 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 02faef7. Configure here.

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[final reviewer] Turned auto-merge off. My last brief covered db67fc0, and two code commits have landed since then (6ef706e, 02faef7), so the brief no longer counts as the review. I'll re-review the current head and post a new brief on a later run, once the head has gone 45 minutes without a push. CI is green and no review threads are open.


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Final review brief (head 02faef7)

What it does. Before it writes a patched wheel into a uv-made PEP 751 pylock, socket-patch checks whether uv would refuse to install that wheel (--no-binary in the lock header, or a uv no-binary setting covering the package) and refuses first, writing nothing (finishes #1603). Since my last brief (db67fc0), a [pip] no-binary settings list is read in order like repeated flags (:none: clears the values before it), and a test pins that a pylock in a uv workspace member is never wired.

Risk: low. It only adds refusals and fails open on malformed TOML or a missing header. The two new commits are small (93 lines): one shared ordered reader and test cases.

Look here

  • upstream/uv.rs:770:`` specifiers_cover, now shared by the header flags (:750) and `settings_no_binary` (:794).````
  • redirect/mod.rs:6245: the hosted refusal (redirect_uv_lock_unsupported), before any content change.
  • vendor/pypi_lock.rs:268: the vendored refusal (pypi_uv_no_binary), during the read-only lock load.
  • mode_migration_pypi.rs:1118: the new workspace-member test, both modes.

Verified

  • Re-read the full delta db67fc0..02faef7 (3 files). CHANGELOG.md is untouched, no debug code, nothing out of scope.
  • The new [":all:", ":none:"] settings case would fail on the old code (it refused); the [":none:", "six"] case still refuses. This closes the open question from my last brief.
  • Ran locally: cargo test -p socket-patch-core --lib -- python_lock_warning_tests no_binary (5 passed) and --test mode_migration_pypi -- uv_pylock (2 passed, including the new test).
  • CI on 02faef7: all checks green, ci-ok and clippy green. Bugbot reviewed 02faef7 with no new issues. Both review threads are resolved.

Changes I made: none.

Open questions: none blocking. As before, a re-run on a project vendored before this change with a --no-binary lock now reports pypi_uv_no_binary instead of "in sync"; that is intended and in the PR description.

Auto-merge (squash) is armed, so approving sends it straight to the merge queue.


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Still Ready for review at the new head (burn-down agent). The earlier label comment covered db67fc0.

  • Head: 02faef7
  • CI: 54/54 settled green on this head (35 success, 19 skipped). No main-wide failures.
  • Bugbot: the Cursor Bugbot check passed on 02faef7. Both review threads are resolved.
  • Mergeable with main. No CHANGELOG.md change.
  • Slack: not announced. This run has no Slack send tool, so the next run will try again.

Generated by Claude Code

Merged via the queue into main with commit b1621f6 Oct 23, 2026
54 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the agent/fix-uv-pylock-no-binary-wire-guard branch October 23, 2026 14:47
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