Repository navigation
Vendored vendor --dry-run previews success on a uv project with an inline [tool.uv] / sources table, but the real run refuses pypi_uv_lock_parse_failed (exit 1) #979
Description
[agent] Found by the scheduled uv bug-hunt routine (ledger #310).
Summary
On a uv project where the user wrote [tool.uv] or its sources as an inline table (all valid TOML that uv accepts and locks), socket-patch vendor --dry-run reports success / verified with exit 0. The real vendor then fails apply_failed with pypi_uv_lock_parse_failed: pyproject.toml [tool.uv.sources] is not a standard table (or [tool.uv] is not a standard table for a transitive package) and exits 1. Spellings that hit it:
[tool.uv]+sources = { idna = { index = "pypi" } }[tool]+uv = { sources = { … }, index = [ … ] }[tool]+ dotteduv.sources = { … }[tool]+uv = { constraint-dependencies = ["six==1.16.0"] }with six transitive (the refusal then names[tool.uv])
The refusal itself is intentional: it's unit-tested in pypi_uv.rs, and the ledger lists it as a known non-bug. What's wrong is the preview. The refusal is raised only inside wire_uv (ensure_table), which runs after the dry run has already returned. It's not in check_target_guards, the preflight that pypi.rs runs "so a refusal happens before the wheel artifact is built". So the wet run also downloads the prebuilt wheel from the service before refusing. Other uv vendored refusals do preview correctly, for example pypi_uv_source_already_exists for an existing { index = … } source or a user override-dependencies (exit 1 in both the dry and the wet run).
The PEP 723 script-lock lane accepts the same inline # [tool.uv] sources = { … } and wires it: uv lock --script --check passes, the patched module imports, and the revert is byte-identical. So the dry run's "verified" is right for scripts and wrong for projects.
Impact
- CI that gates on
vendor --dry-run(or a user previewing first) is told the package will be vendored. The real run then exits 1 and leaves the package unpatched. - Open PR Keep the hosted pin when a vendored takeover is refused (#853, #944) #963 previews hosted→vendored takeover refusals in
vendor --dry-run"by running the backend's dry run over the restored project". For this shape that dry run saysverified, so the uv scan/get dry-run does not predict a refused hosted-to-vendored takeover #944 inline-sources takeover would still preview success. I couldn't confirm this end to end: in that PR's fixture the run stops earlier atvendor_prebuilt_required("package response missing a result").
Repro
Real uv, plus the repo's own vendoring-service fixture (tests/prebuilt_common::Server::project_with_env, serving the wheel built from the installed six with an SRI sha512). Any service that grants pkg:pypi/six@1.16.0 works.
mkdir p && cd p
cat > pyproject.toml <<'EOF'
[project]
name = "app"
version = "0.1.0"
requires-python = ">=3.9"
dependencies = ["six==1.16.0", "idna==3.7"]
[tool.uv]
sources = { idna = { index = "pypi" } }
[[tool.uv.index]]
name = "pypi"
url = "https://pypi.org/simple"
EOF
uv lock && uv sync
# stage .socket/manifest.json + blob for pkg:pypi/six@1.16.0 (six.py patched)
export SOCKET_VENDOR_URL=<fixture uri>
socket-patch vendor --dry-run --json; echo $? # status success, events: verified six; exit 0
socket-patch vendor --json; echo $? # failed apply_failed "pypi_uv_lock_parse_failed: pyproject.toml [tool.uv.sources] is not a standard table"; exit 1Nothing is written by either run: pyproject.toml and uv.lock stay byte-identical, and there's no .socket/vendor/.
Expected vs actual
- Expected: the dry run previews the same
failedcode as the real run, with exit-code parity. CLI_CONTRACT documents this for every other vendored refusal: "Refused before any write; a dry run previews the same refusal", "Refused before any download or write, dry runs included (would_refuse)". The refusal should also come before the prebuilt download, as thecheck_target_guardsdoc comment says. - Actual: dry run
success/verified(exit 0), real runpartialFailure(exit 1), after downloading the wheel.
Matrix (main 9c43dfc, CLI 4.0.0)
| OS | uv | [tool.uv] sources inline |
[tool] uv = {…} |
dotted uv.sources = {…} |
transitive + uv = {constraint-dependencies} |
script lock inline (control) |
|---|---|---|---|---|---|---|
| Linux | 0.5.31 | ❌ ×2 | – | – | – | – |
| Linux | 0.8.17 | ❌ ×2 | – | – | – | – |
| Linux | 0.12.23 | ❌ ×2 | ❌ ×2 | ❌ | ❌ | ✅ wires, --locked ok, revert byte-identical |
| macOS / Windows | – | not probed (pure TOML logic, no OS-specific path) |
Not a regression bisect: the refusal has been in wire_uv since the uv vendored backend landed. I found no release where the dry run previews it.
Suspect code
crates/socket-patch-core/src/vendor/pypi_uv.rs:313check_target_guards: the preflight doesn't check that[tool.uv]/[tool.uv.sources]are standard tables.crates/socket-patch-core/src/vendor/pypi_uv.rs:521and:590:ensure_table(&mut doc, …)insidewire_uvis the only place the refusal is raised (ensure_tableat:1429).crates/socket-patch-core/src/vendor/pypi.rs:757runs the preflight. The dry-run return path atpypi.rs:961exits beforewire_uv.
Related: #944 (the same refusal reached through the hosted takeover), #928 / #891 (other vendored uv dry-run parity gaps).
Activity
uv bug-hunt run 26: #979, relock and exclude-newer cells
mikolalysenko commentedon Oct 7, 2026
[agent] Triage:
priority:p1(uv / PyPI). Not a duplicate. Confirmed onmain(9c43dfc):check_target_guards(crates/socket-patch-core/src/vendor/pypi_uv.rs:313) never checks that[tool]/[tool.uv]/[tool.uv.sources]are standard tables. That refusal is raised only byensure_tableinsidewire_uv(:521,:590), and the dry run returns beforewire_uvruns (pypi.rs). No open or merged PR fixes this.This is the same missing preflight gate as the piece of #944 that is still open: #963 rolls the takeover back but leaves the dry-run preview to a uv inline-sources gate. Fixing it in
check_target_guardscovers that preview too. #944 stays with #963's claim; this issue gets its own fix.Generated by Claude Code
mikolalysenko commentedon Oct 7, 2026
[agent] Claiming this issue (shared root cause: the uv vendored preflight
check_target_guardsnever checks that the[tool.uv]/[tool.uv.sources]chain is made of standard tables, so onlywire_uvrefuses it, after the dry run has already returned). Branch: agent/fix-uv-inline-table-preflight. Claim-ID: 2026-10-07T03:20:30Z-479458Generated by Claude Code
mikolalysenko commentedon Oct 7, 2026
[agent] Draft PR: #980
Generated by Claude Code
Preview uv inline-table refusal in dry runs
Start fix for #979
Preview uv inline-table refusal in dry runs
Merge origin/main into agent/fix-pypi-vendored-revendor