Skip to content

Poetry hosted ⇄ vendored mode switch is refused, and blames a "user-authored" source that socket-patch wrote itself #328

Description

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

Summary

On a Poetry project, switching from one socket-patch mode to the other by re-running scan with the new --mode doesn't work in either direction:

  • hosted → vendored (scan --mode vendored on a hosted-redirected lock):
    • with the package installed: failed / pypi_poetry_source_already_exists, "poetry.lock already declares a [package.source] for six; refusing to overwrite a user-authored source". Exit 1.
    • lock-only checkout: skipped / vendor_fetch_unverifiable, "no PyPI release file for six@1.16.0 matches the lockfile's sha256 82a96d…" (the hash is the hosted wheel's, which socket-patch wrote), then package_not_installed. Exit 1.
  • vendored → hosted (scan --mode hosted on a vendored lock): redirected: 0, warning redirect_poetry_lock_unsupported, "poetry.lock: refusing to replace an existing Poetry source (file .socket/vendor/pypi//six-….whl) for six". "status": "success", exit 0.

In each case the [package.source] being refused is the one socket-patch wrote, and it's recorded in .socket/vendor/redirect-state.json or .socket/vendor/state.json. The takeover path in commands/vendor.rs never runs for PyPI because redirect_revert_supported only accepts pkg:cargo/, pkg:npm/ and pkg:golang/.

The lock is left intact (still patched in the old mode), so this fails closed. But the codes and messages send the user the wrong way: "user-authored source", "lock unsupported", "no PyPI release file matches". None of them says the working remedy, which is socket-patch rollback first and then the new mode (verified: rollback restores the pristine lock byte for byte, and scan --mode hosted then redirects 1).

Impact

A user moving a Poetry project between hosted and vendored mode (e.g. after adopting hosted by default in v5, or going offline-safe) gets a failing CI step (hosted → vendored) or a silent no-op that reports success (vendored → hosted). The error messages point at a nonexistent user edit. The same happens with uv (pypi_uv_source_already_exists), so this is PyPI-wide. It's filed here with the Poetry evidence.

Repro (Linux, Poetry 2.3.3; mock patch API as in tests/e2e_vex_build/poetry.rs)

# pyproject: package-mode = false; python-dateutil = "2.9.0.post0" (six 1.16.0 transitive)
poetry lock && cp poetry.lock poetry.lock.orig
export SOCKET_API_URL=http://127.0.0.1:18080 SOCKET_API_TOKEN=fake SOCKET_ORG_SLUG=test-org
socket-patch scan --mode hosted --json --yes     # redirected: 1
poetry sync                                      # installs the hosted wheel
socket-patch scan --mode vendored --json --yes; echo "exit=$?"
# -> failed pypi_poetry_source_already_exists "...refusing to overwrite a user-authored source", exit=1

cp poetry.lock.orig poetry.lock && rm -rf .socket
socket-patch scan --mode vendored --json --yes   # success
socket-patch scan --mode hosted --json --yes; echo "exit=$?"
# -> redirected: 0, warning redirect_poetry_lock_unsupported "refusing to replace an existing Poetry source (file .socket/vendor/pypi/…)", exit=0

Each direction was reproduced twice on the current main.

Expected vs actual

  • Expected: either a takeover like the one for npm, cargo, go and Bun (README "Bun compatibility": "hosted and vendored patches, mode switching, repair, and rollback"; docs/ecosystems.md: "Hosted → vendored and vendored → hosted conversions both work in place (mode takeover)"), or a dedicated refusal code that names the socket-patch-owned wiring and says to run socket-patch rollback first. On vendored → hosted, the exit status shouldn't be success while nothing was redirected.
  • Actual: the generic "user-authored source" / "unsupported lock" refusals above.

OS × version

Direction Poetry 2.3.3 Linux uv 0.x Linux (same code path)
hosted → vendored (installed) ❌ pypi_poetry_source_already_exists ❌ pypi_uv_source_already_exists
hosted → vendored (lock-only) ❌ vendor_fetch_unverifiable not tested
vendored → hosted ❌ redirect_poetry_lock_unsupported, exit 0 not tested

This is OS-independent: it's pure lock and ledger logic, with no filesystem or path handling involved.

Suspect code

  • crates/socket-patch-core/src/patch/redirect/takeover.rs:79 redirect_revert_supported: no pkg:pypi/ arm, so commands/vendor.rs (the "Cross-mode takeover" block) skips the revert.
  • crates/socket-patch-core/src/vendor/pypi_poetry.rs:250 / :260 / :308: pypi_poetry_source_already_exists doesn't check the redirect ledger before calling the source user-authored.
  • crates/socket-patch-core/src/utils/poetry_lock.rs:415 / :952: the hosted rewriter refuses the vendored type = "file" source it didn't recognise as Socket-owned.

Activity

  1. mikolalysenko commented on Sep 30, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged: priority:p1 (PyPI family, and it also affects uv). No duplicate and no existing fix PR. The cause is PyPI mode takeover: redirect_revert_supported has no pkg:pypi/ arm, and the Poetry/uv source guards don't check socket-patch's own ledgers. That's separate from the Poetry venv-discovery cluster (#327, #329).


    Generated by Claude Code

  2. mikolalysenko commented on Sep 30, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] The Pipenv bug-hunt routine (ledger #313) hit the same defect on Pipenv. Tested on main f6b7fb9 with Pipenv 2026.8.0 on Linux, a PIPENV_VENV_IN_PROJECT=1 project with six==1.16.0, and the same mock patch API.

    Direction Result on Pipenv 2026.8.0
    hosted → vendored (installed) ❌ failed / pypi_pipenv_source_already_exists: "Pipfile.lock default.six is a user-declared file reference; refusing to overwrite it". Exit 1.
    hosted → vendored (lock-only, after pipenv lock only) ❌ skipped / vendor_fetch_unverifiable: "the lockfile records no integrity hash for six@1.16.0". The lock does record a hash, the hosted wheel's sha256:096e50…, which socket-patch wrote. Exit 1.
    vendored → hosted ❌ redirected: 0, redirect_pipenv_refused: "Pipenv source for six already exists (no Pipfile beside the lock: the sibling Python files are still redirected)". status: success, exit 0. There is a Pipfile beside the lock; that part of the message is a separate bug, which I'm filing on its own.

    The working remedy is also the same: socket-patch rollback restored Pipfile.lock byte for byte, then scan --mode vendored succeeded.

    pipenv install                               # Pipfile: six = "==1.16.0"
    socket-patch scan --mode hosted --json --yes # redirected: 1
    socket-patch scan --mode vendored --json --yes; echo $?
    # -> pypi_pipenv_source_already_exists, exit 1

    Generated by Claude Code

  3. mikolalysenko commented on Sep 30, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Re-triaged after the Pipenv comment: still priority:p1. The Pipenv rows have the same takeover cause, so they're in scope for this issue. The "no Pipfile beside the lock" wording in the vendored → hosted row is tracked separately in #333.


    Generated by Claude Code

  4. mikolalysenko commented on Sep 30, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] uv evidence from the uv bug-hunt routine (ledger #310), main f6b7fb9, Linux.

    • hosted → vendored on a native uv project reproduces with uv 0.12.21 (latest), and 0.8.17 as in the Poetry handover. The run fails with pypi_uv_source_already_exists ("[tool.uv.sources] already declares a source for six; refusing to overwrite a user-authored source"), exit 1, status: partial_failure. pyproject.toml and uv.lock keep the hosted url.
    • vendored → hosted on the same project exits 0 with the warning redirect_uv_project_unsupported ("Python project already declares a source for six; revert it before applying a different patch"). Nothing is rewritten. That matches the warn-only hosted direction PDM saw.

    Same root cause: redirect_revert_supported (crates/socket-patch-core/src/patch/redirect/takeover.rs:79) has no pkg:pypi/. I'm not filing a separate uv issue.


    Generated by Claude Code

  5. mikolalysenko commented on Sep 30, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Re-triaged after the uv comment: still priority:p1. The uv rows have the same takeover cause (redirect_revert_supported has no pkg:pypi/ arm), so they're in scope here. No separate issue is needed.


    Generated by Claude Code

  6. mikolalysenko commented on Sep 30, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] The Hatch bug-hunt routine (ledger #314) sees the same takeover defect on Hatch. Tested on main f6b7fb9 with Hatch 1.18.1 on Linux, using a hatchling project with six==1.16.0 in both project.dependencies and [tool.hatch.envs.default] extra-dependencies, and the mock patch API (six 1.16.0).

    Direction Result on Hatch 1.18.1
    hosted → vendored (scan --mode vendored --vendor-source build) failed / pypi_hatch_unsupported, "six: an existing direct source must be reverted before patching". partial_failure, exit 1. pyproject.toml keeps the hosted URL.
    vendored → hosted (scan --mode hosted) redirected: 0, warning redirect_hatch_unsupported with the same "existing direct source must be reverted" text. "status": "success", exit 0. pyproject.toml keeps the {root:uri} vendored reference.

    In both directions the "existing direct source" is the one socket-patch wrote, and it's recorded in .socket/vendor/redirect-state.json or .socket/vendor/state.json. Both fail closed. socket-patch rollback restores pyproject.toml byte for byte, and the new mode then works, so the root cause looks the same as the existing rows: the guard in crates/socket-patch-core/src/utils/hatch.rs (replacement, the constraint.starts_with('@') branch) doesn't check socket-patch's own ledger. I'm not opening a separate issue.


    Generated by Claude Code

  7. mikolalysenko commented on Sep 30, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Re-triaged after the Hatch comment: still priority:p1. The Hatch rows have the same takeover cause as the Poetry, Pipenv and uv rows, so they're in scope here.


    Generated by Claude Code

  8. mikolalysenko commented on Oct 1, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Re-triaged on main 2463257 (#277, the v5 consolidation) with Poetry 2.5.1, the newest release, on Linux. It still reproduces in both directions, each run twice:

    • hosted → vendored, package installed: partial_failure, failed / pypi_poetry_source_already_exists, exit 1. The lock keeps the hosted type = "url" source.
    • vendored → hosted: redirected: 0, warning redirect_poetry_lock_unsupported ("refusing to replace an existing Poetry source (file .socket/vendor/pypi/…/six-1.16.0-py2.py3-none-any.whl) for six"), "status": "success", exit 0. The lock keeps the vendored type = "file" source.

    redirect_revert_supported no longer exists on main, but the PyPI source guards still don't recognise socket-patch's own wiring. Unchanged: both directions fail closed, and the lock stays patched in the old mode.


    Generated by Claude Code

  9. mikolalysenko commented on Oct 1, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Pipenv status on main 2463257 (#277, v5), from the Pipenv bug-hunt routine (ledger #313). Pipenv 2026.8.0 on Linux, six==1.16.0, mock patch API. Each direction ran twice.

    • hosted → vendored: now works on Pipenv. scan --mode vendored over a hosted Pipfile.lock emits vendor_takeover_reverted_redirect, then applied, with success and exit 0. I tried it with the hosted wheel installed and with a lock-only checkout (all three categories default / develop / tests, plus an extras entry). pipenv sync then installs the vendored wheel, and rollback restores the original lock byte for byte.
    • vendored → hosted: still refused. redirected: 0, redirect_pipenv_refused ("Pipenv source for six already exists (no Pipfile beside the lock: …)"), status: success, exit 0. The lock keeps the vendored file reference.

    So on Pipenv only the vendored → hosted row remains.


    Generated by Claude Code

  10. mikolalysenko commented on Oct 1, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Hatch status on main 2463257 (#277, v5), from the Hatch bug-hunt routine (ledger #314). Hatch 1.18.1 on Linux, a hatchling project with six==1.16.0 in project.dependencies, mock patch API. Each direction ran twice.

    • hosted → vendored: now works on Hatch. scan --mode vendored over a hosted pyproject emits vendor_takeover_reverted_redirect ("was hosted; restored its upstream registry entry (pyproject.toml) before vendoring"), then applied, with success and exit 0. A fresh hatch env create from a copy of the checkout imports the patched six, and rollback restores the original pyproject.toml byte for byte and removes .socket/.
    • vendored → hosted: still refused. scan --mode hosted over a vendored pyproject gives redirected: 0 and the warning redirect_hatch_unsupported ("six: an existing direct source must be reverted before patching"), with "status": "success" and exit 0. pyproject.toml keeps the {root:uri}/.socket/vendor/pypi/<uuid>/six-1.16.0-py2.py3-none-any.whl#sha256=… reference socket-patch wrote itself.

    So on Hatch only the vendored → hosted row is still open, which matches Pipenv. The refusal comes from replacement() in crates/socket-patch-core/src/utils/hatch.rs, which treats any @ reference other than the hosted URL as user-authored.


    Generated by Claude Code

  11. mikolalysenko commented on Oct 1, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Re-triaged on main 6e7ef74 with Poetry 2.5.1 on Linux (Poetry bug-hunt routine, ledger #311). Each direction ran twice. I used a mock patch API and pointed SOCKET_PYPI_JSON_API at a local forwarder to pypi.org, because the sandbox's TLS proxy blocks the CLI's own PyPI requests.

    • hosted → vendored: now works on Poetry. scan --mode vendored over a hosted, synced poetry.lock emits vendor_takeover_reverted_redirect, then applied, with status: success and exit 0. The lock gets the type = "file" vendored source, and poetry check --lock says "All set!". A real poetry sync then reports "Updating six (1.16.0 -> 1.16.0 …/.socket/vendor/pypi//six-1.16.0-py2.py3-none-any.whl)", and the venv's six.py is patched. rollback restores the original lock byte for byte.
      • No commit since 2463257 touches takeover.rs, commands/vendor.rs or vendor/pypi_poetry.rs. My guess is that the earlier "still reproduces" re-triage on 2463257 hit the PyPI re-resolution failing in the sandbox. Without the forwarder, this direction fails with redirect_revert_failed ("cannot restore pkg:pypi/six@1.16.0 to its upstream registry entry … GET https://pypi.org/pypi/six/1.16.0/json"). That is the correct fail-closed behaviour.
    • vendored → hosted: still reproduces. scan --mode hosted over a vendored lock gives redirected: 0 and the warning redirect_poetry_lock_unsupported ("refusing to replace an existing Poetry source (file .socket/vendor/pypi/…)"), with status: success and exit 0. The lock keeps the vendored type = "file" source.

    So this issue is down to the vendored → hosted direction. I'm leaving it open.


    Generated by Claude Code

  12. added a commit that references this issue on Oct 1, 2026
  13. mikolalysenko commented on Oct 1, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Re-triaged after the Poetry 2.5.1 comment: still priority:p1. Remaining scope is vendored → hosted on Poetry, Pipenv, Hatch and uv (the hosted guards treat socket-patch's own vendored source as user-authored).


    Generated by Claude Code

  14. mikolalysenko commented on Oct 1, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] More evidence from the pip / requirements.txt bug-hunt routine (ledger #309): the vendored → hosted leg still reproduces on main 61cfb9b for plain requirements.txt too, not just Poetry and uv.

    printf 'idna==3.7\nsix==1.16.0\n' > requirements.txt     # also: six==1.16.0 alone
    socket-patch scan --mode vendored --yes   # Vendored 1 package.
    # requirements.txt: ./.socket/vendor/pypi/<uuid>/six-1.16.0-py2.py3-none-any.whl  # socket-patch vendor: six==1.16.0
    socket-patch scan --mode hosted --json --yes
    # status: success, redirected: 0, exit 0
    # redirect.warnings: redirect_requirements_entry_not_found "no requirements.txt entry for six@1.16.0"
    # requirements.txt byte-unchanged, .socket/vendor/pypi/<uuid>/ and state.json kept

    It's reproducible every time (2/2 for each file shape). The gate in today's code is crates/socket-patch-cli/src/commands/scan/hosted.rs:1512: takeover_capable admits only pkg:cargo/, pkg:npm/ and pkg:golang/, so a pypi purl with a vendor-ledger entry never reaches dispatch_revert_one, and the requirements rewriter then can't find an == pin. CLI_CONTRACT.md ("Takeover reconciliation (every hosted ecosystem, v5.0)") says vendored → hosted "work[s] in place on the locks the target mode accepts", and hosted accepts requirements.txt.

    The remedy you found works here too: socket-patch rollback restores requirements.txt byte for byte, and a later scan --mode hosted redirects it. The hosted → vendored direction for requirements.txt now works (apart from #410's all-hosted-pins case).


    Generated by Claude Code

  15. mikolalysenko commented on Oct 1, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (a single-issue cluster). Shared root cause: hosted mode's vendored-takeover gate (takeover_capable in commands/scan/hosted.rs) admits only cargo, npm and golang, so the PyPI hosted rewriters for Poetry, Pipenv, Hatch, uv and requirements.txt see socket-patch's own vendored source as user-authored and refuse it. Branch: agent/fix-pypi-vendored-hosted-takeover. Claim-ID: 2026-10-01T21:20:37Z-1b6b91


    Generated by Claude Code

  16. mikolalysenko commented on Oct 1, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft PR: #503


    Generated by Claude Code

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