Skip to content

Hosted and vendored uv wiring ignore no-sources = true: scan and vendor report success, uv sync --locked then fails, and a plain uv sync reinstalls the unpatched wheel #564

Description

[agent] Found by the scheduled uv bug-hunt routine (ledger #310).

Summary

Hosted (scan --mode hosted) and vendored (vendor) wiring for uv projects both route the patched package through a [tool.uv.sources] entry, and they repoint uv.lock to match. When the project sets no-sources = true (in [tool.uv] of pyproject.toml or in a project uv.toml), uv ignores [tool.uv.sources] entirely. Neither writer checks for this. Both report success / exit 0 and leave a lock that uv considers outdated.

Impact

  • Locked CI breaks right after wiring: uv lock --check and uv sync --locked fail ("The lockfile at uv.lock needs to be updated, but --locked was provided"). That's exit 1 on uv 0.8.17 / 0.12.22 and exit 2 on 0.5.31.
  • The patch is silently lost: a plain uv sync (or uv lock) relocks six to registry = "https://pypi.org/simple". On a fresh checkout it installs the unpatched upstream wheel. Only uv sync --frozen installs the patch.
  • No warning or refusal is emitted, so the user believes the project is patched.

Repro (Linux, main 61cfb9b, real uv 0.8.17)

cat > pyproject.toml <<'EOF'
[project]
name = "ns"
version = "0.1.0"
requires-python = ">=3.9"
dependencies = ["six==1.16.0", "idna==3.7"]

[tool.uv]
no-sources = true
EOF
uv lock && uv sync --no-install-project
# a free six 1.16.0 patch: .socket/manifest.json + blob for vendor, or the hosted routes (local mock patch API)
socket-patch vendor --json            # status success, applied 1; writes [tool.uv.sources] six = { path = ".socket/vendor/pypi/<uuid>/six-1.16.0-py2.py3-none-any.whl" }
#   or: socket-patch scan --mode hosted --json --yes --api-url $API ...   # status success, redirected 1; writes six = { url = "<hosted wheel>" }
uv lock --check                        # exit 1: lock needs to be updated
rm -rf .venv && uv sync --locked --no-install-project   # exit 1
rm -rf .venv && uv sync --frozen --no-install-project   # installs the patched wheel
rm -rf .venv && uv sync --no-install-project            # relocks six to the PyPI registry; installs UNPATCHED six

Control: the same project without no-sources = true passes uv lock --check and uv sync --locked, and a plain uv sync keeps the patch (hosted and vendored, same run). Putting no-sources = true in a project uv.toml instead gives the same failure (vendored, 0.8.17).

Expected vs actual

  • Expected: docs/testing/uv-compatibility.md says the [tool.uv.sources] entry "carr[ies] the redirect (verified with --frozen, --locked, and ordinary installs)". When a project setting stops uv from honouring that entry, the writers should refuse or at least warn before writing anything, the way they already refuse other configurations they can't wire (redirect_uv_project_unsupported, pypi_uv_source_already_exists). That would leave the project untouched.
  • Actual: both modes write the source plus the lock repoint and report success. The result fails --locked, and an ordinary install drops the patch.

OS × version (Linux; the check is config-driven, so it doesn't depend on the OS)

Mode uv 0.5.31 uv 0.8.17 uv 0.12.22
vendored, [tool.uv] no-sources = true ❌ ❌ (reproduced 2×) ❌
hosted, [tool.uv] no-sources = true ❌ ❌ (reproduced 2×) ❌
vendored, uv.toml no-sources = true – ❌ –
control (no no-sources), hosted and vendored ✅ ✅ ✅

Not bisected. git grep no-sources on main finds nothing in the crates or docs.

Suspect code

  • crates/socket-patch-core/src/vendor/pypi_uv.rs:589: ensure_table(&mut doc, &["tool", "uv", "sources"]) is written unconditionally. Nothing reads tool.uv.no-sources or uv.toml.
  • crates/socket-patch-core/src/utils/python_script.rs:142 (rewrite_sources, called from rewrite_project_metadata at :230): the same for the hosted path.

Activity

  1. mikolalysenko commented on Oct 2, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triage: priority:p1 (uv). Confirmed on main: neither the hosted nor the vendored uv [tool.uv.sources] writer checks no-sources (no occurrence of no-sources in socket-patch-core). Distinct from open #545 (sources-table spelling); no open PR covers it.


    Generated by Claude Code

  2. mikolalysenko commented on Oct 5, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Re-triage from the uv bug-hunt routine (ledger #310): this still reproduces on main 99f61d2 on Linux, against the local mock.

    Shape: dependencies = ["six==1.16.0", "attrs>=20"] plus [tool.uv] no-sources = true, then uv lock, uv sync, scan --mode <mode> --yes, uv sync --locked, then a plain uv sync and a check of whether the installed six carries the patch marker.

    uv hosted vendored
    0.5.31 scan exit 0 (only redirect_pypi_stale_install), --locked exit 2, plain sync installs unpatched six scan exit 0, no warning, --locked exit 2, plain sync unpatched
    0.8.17 same, --locked exit 1 same
    0.12.23 same, --locked exit 1 ("The lockfile at uv.lock needs to be updated") same

    Generated by Claude Code

  3. added a commit that references this issue on Oct 5, 2026
  4. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    v5 triage: P2, not a release blocker. Retain the explicit uv no-sources configuration conflict at P2; it is a supported-settings follow-up, not a demand for new recovery machinery.

    This follows the maintainer's release scope: one normally completing CLI instance, prioritizing valid-lockfile patch/install behavior, compatibility, and actionable CLI UX.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions