[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] + dotted uv.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
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 1
Nothing 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
failed code 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 the check_target_guards doc comment says.
- Actual: dry run
success / verified (exit 0), real run partialFailure (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:313 check_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:521 and :590: ensure_table(&mut doc, …) inside wire_uv is the only place the refusal is raised (ensure_table at :1429).
crates/socket-patch-core/src/vendor/pypi.rs:757 runs the preflight. The dry-run return path at pypi.rs:961 exits before wire_uv.
Related: #944 (the same refusal reached through the hosted takeover), #928 / #891 (other vendored uv dry-run parity gaps).
[agent] Found by the scheduled uv bug-hunt routine (ledger #310).
Summary
On a uv project where the user wrote
[tool.uv]or itssourcesas an inline table (all valid TOML that uv accepts and locks),socket-patch vendor --dry-runreportssuccess/verifiedwith exit 0. The realvendorthen failsapply_failedwithpypi_uv_lock_parse_failed: pyproject.toml [tool.uv.sources] is not a standard table(or[tool.uv] is not a standard tablefor 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 insidewire_uv(ensure_table), which runs after the dry run has already returned. It's not incheck_target_guards, the preflight thatpypi.rsruns "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 examplepypi_uv_source_already_existsfor an existing{ index = … }source or a useroverride-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 --checkpasses, 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
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.vendor --dry-run"by running the backend's dry run over the restored project". For this shape that dry run saysverified, so the uv hosted → vendored takeover (scan/get --mode vendored) restores the package to PyPI before the uv vendored refusals run, so an inline[tool.uv] sources = {…}table (or a #928 marker split) leaves it unpatched in both modes, while --dry-run previews would_vendor #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 grantspkg:pypi/six@1.16.0works.Nothing is written by either run: pyproject.toml and uv.lock stay byte-identical, and there's no
.socket/vendor/.Expected vs actual
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.success/verified(exit 0), real runpartialFailure(exit 1), after downloading the wheel.Matrix (main
9c43dfc, CLI 4.0.0)[tool.uv]sources inline[tool] uv = {…}uv.sources = {…}uv = {constraint-dependencies}--lockedok, revert byte-identicalNot a regression bisect: the refusal has been in
wire_uvsince 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).