Repository navigation
Vendored Poetry never re-vendors to a superseding patch: re-scan exits 1 with pypi_poetry_source_already_exists, while --dry-run previews would_revendor, and the project keeps installing the old patch #1136
Description
[agent] Found by the scheduled Poetry bug-hunt routine (ledger #311).
Summary
A Poetry project vendored with patch uuid A can't move to a newer patch uuid B for the same name@version. When the patch API supersedes A with B, scan --mode vendored (and get <B> --mode vendored) downloads B (download.patches[].oldUuid = A, updates: [{oldUuid: A, newUuid: B}]), then the vendor step fails with:
failed pkg:pypi/six@1.16.0?artifact_id=py2-py3-none-any-whl pypi_poetry_source_already_exists
"poetry.lock already routes six through .socket/vendor/pypi/<A> (an earlier socket-patch vendor); run `socket-patch vendor --revert` for it and re-vendor"
The run ends status: partial_failure, exit 1. poetry.lock stays wired to .socket/vendor/pypi/<A>/…, so every poetry install keeps installing the old patch. The same run's --dry-run previews action: "would_revendor" (+ oldUuid), status success, exit 0.
#943 fixed this for uv, script locks and Hatch (#742, #650); #825 fixed it for Pipenv and #766 for requirements.txt. The Poetry arm of the vendored orchestrator was left out.
Impact
- A superseding patch (for example a fixed patch, or one covering more advisories) never reaches vendored Poetry projects. Every scheduled
scan --mode vendoredin CI fails with exit 1 until someone intervenes by hand. - The error tells the user to revert a wiring that socket-patch wrote itself. Following that advice isn't enough: when the project venv still holds the old patch's bytes, the re-scan after
vendor --revertfails again withpackage_not_installed("no installed package found on disk", exit 1), and the lock is left with no patch at all. The working sequence isvendor --revert, thenpoetry install(pristine bytes back), thenscan --mode vendored. That second failure is the class uv hosted → vendored takeover onto a newer patch uuid fails "no installed package found on disk" (exit 1) when the venv holds the hosted patch's bytes, while --dry-run previews success #1105 tracks for uv. - The dry run promises a re-vendor the wet run refuses.
Repro (Linux, real Poetry, local mock patch API)
The mock answers POST …/patches/{batch,package}, GET …/by-package/…, GET …/view/<uuid> and serves a deterministic patched six-1.16.0-py2.py3-none-any.whl (sha256 + sha512 in the grant). It first offers uuid A (3c5e7a9b-…, six.SOCKET_PATCHED = 1), then switches to offer only uuid B (5e7a9c1d-…, SOCKET_PATCHED = 2) for pkg:pypi/six@1.16.0.
cat > pyproject.toml <<'EOF'
[tool.poetry]
name = "demo"
version = "0.1.0"
description = ""
authors = ["x <x@example.com>"]
[tool.poetry.dependencies]
python = "^3.9"
six = "1.16.0"
EOF
printf '[virtualenvs]\nin-project = true\n' > poetry.toml
poetry lock
SP="--api-url $M --org o --api-token x --json --ecosystems pypi"
socket-patch scan --mode vendored $SP # uuid A vendored, exit 0
# patch API now offers only uuid B
socket-patch scan --mode vendored $SP --dry-run # vendor.events: would_revendor (oldUuid A), exit 0
socket-patch scan --mode vendored $SP # exit 1, partial_failure, pypi_poetry_source_already_exists
grep -o 'vendor/pypi/[0-9a-f]\{8\}' poetry.lock # still 3c5e7a9b (A)
rm -rf .venv && poetry install --no-root
.venv/bin/python -c 'import six; print(six.SOCKET_PATCHED)' # 1 (old patch)Expected vs actual
- Expected (crates/socket-patch-cli/CLI_CONTRACT.md,
scan --vendorparagraph): "A package the ledger holds at an older patch uuid is still re-vendored automatically when discovery selects the newer patch (its old uuid dir is removed —vendor_stale_artifact_removed)". The same paragraph says--dry-runwould_revendorpreviews that re-vendor. - Actual: the wet run refuses with exit 1 and keeps uuid A wired. The dry run and the wet run disagree.
Matrix (Linux, main 3b4ac84)
| Poetry | lock-version | EOL | --dry-run |
wet re-scan | fresh install gets |
|---|---|---|---|---|---|
| 1.1.15 | 1.1 | LF | would_revendor, exit 0 | exit 1 | old patch (A) |
| 1.8.5 | 2.0 | LF | would_revendor, exit 0 | exit 1 | old patch (A) |
| 1.8.5 | 2.0 | CRLF | would_revendor, exit 0 | exit 1 | old patch (A) |
| 2.0.1 | 2.1 | LF | would_revendor, exit 0 | exit 1 | old patch (A) |
| 2.4.3 | 2.1 | LF | would_revendor, exit 0 | exit 1 | old patch (A) |
| 2.4.3 | 2.1 | CRLF | would_revendor, exit 0 | exit 1 | old patch (A) |
| 2.4.3 | get <B> --mode vendored |
LF | – | exit 1 (same code) | – |
| control: hosted, same mock, 2.4.3 (LF + CRLF) | re-pins to B, exit 0 | new patch (B); remove / rollback restore the lock byte for byte |
Each failing cell reproduced on two separate runs. The behaviour comes from the lock writer, not from Poetry or the OS. Not bisected: the refusal has been in check_target_guards since vendored Poetry support landed. #943 is where the sibling flavors gained the replay path.
Suspect code
crates/socket-patch-core/src/vendor/pypi.rs:926-946: thePypiFlavor::Poetryarm maps everycheck_target_guardserror straight torefused(..). The uv, script-lock and Hatch arms (pypi.rs:867/887/910) route the same "our own older wiring" refusal throughsupersede_or_refuse(pypi.rs:1586), andsuperseded_files(pypi.rs:1666) has no Poetry entry.crates/socket-patch-core/src/vendor/pypi_poetry.rs:243-257: the "Ours, but a STALE patch generation" branch refuses withpypi_poetry_source_already_existsinstead of letting the orchestrator replay the recorded entry's revert and wire uuid B.- The unit test
splice_flavor_stale_uuid_vendor_refuses_through_orchestrator(pypi.rs:7462) pins the refusal forpoetry.lock, which contradicts the contract. - The PDM arm (
pypi.rs:948) looks the same; I've handed that over to the PDM routine rather than folding it in here.
No probe runs: macOS and Windows probe branches are blocked for this routine (see ledger #311). The defect is platform-independent code.
Activity
poetry: run 16 ledger (#1136, #1105 comment, pdm handover)
mikolalysenko commentedon Oct 8, 2026
[agent] Triage: priority:p1 (PyPI/Poetry). Confirmed on main 3b4ac84: the
PypiFlavor::Poetryarm incrates/socket-patch-core/src/vendor/pypi.rs(around line 926) maps everycheck_target_guardserror straight torefused(..), while the uv, script-lock and Hatch arms route it throughsupersede_or_refuse.SUPERSEDABLE_REFUSALSalso has nopypi_poetry_source_already_exists. The PDM arm right below it has the same gap. Not a duplicate; no open PR covers it.Generated by Claude Code
mikolalysenko commentedon Oct 8, 2026
[agent] Claiming this issue (shared root cause: the Poetry and PDM arms of the PyPI vendored orchestrator never route their own older-uuid refusal through supersede_or_refuse). Branch: agent/fix-pypi-poetry-pdm-supersede. Claim-ID: 2026-10-08T14:22:24Z-0aff93
Generated by Claude Code
mikolalysenko commentedon Oct 8, 2026
[agent] Draft PR: #1137
Generated by Claude Code
Re-vendor Poetry and PDM to a superseding patch
Test Poetry re-vendor over a patched venv
E2e: real Poetry re-vendors to a superseding patch
Recreate the venv before installing patch A