Skip to content

Fix PyPI hosted takeover un-vendoring before refusal (#723, #945) - #946

Merged
Mikola Lysenko (mikolalysenko) merged 14 commits into
mainfrom
agent/fix-pypi-hosted-takeover-preflight
Oct 8, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 14 commits into
mainfrom
agent/fix-pypi-hosted-takeover-preflight

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

LLM Description written by Claude Code:claude-opus-5-5

Fixes #723
Fixes #945

Summary

On a uv or Poetry project that socket-patch vendored, scan --mode hosted could delete the vendored patch and then fail to pin the hosted one. The package ended up in neither mode, and the next install got the unpatched release. --dry-run promised a clean takeover in the same situation. With this fix the takeover is refused up front, in the dry run and the wet run alike, and the package stays vendored and patched (exit 0).

Root cause

The vendored → hosted takeover reverts a purl's vendored wiring first, then asks the hosted rewriter to pin it. The takeover_refusal gate in crates/socket-patch-cli/src/commands/scan/hosted.rs is supposed to refuse, before the revert, any purl the hosted rewriter can't reach. For pkg:pypi/ it only ran preflight_requirements_takeover, which covers requirements.txt. Nothing checked the uv or Poetry rewriter:

Fix

  • New core preflight, patch::redirect::preflight_pypi_takeover(root, entry) in pypi_takeover.rs. It dispatches on the ledger entry's flavor:
    • requirements: the existing preflight_requirements_takeover
    • uv: refuses with the new code redirect_uv_takeover_version_unreachable when a recorded uv_lock_package original unit has a version different from the patch's. The comparison is exact, the same as the hosted planner's matching_package.
    • poetry: reads the wired poetry.lock and refuses with redirect_poetry_lock_unsupported when it's a 0.x lock. The revert never changes the lock format.
  • scan/hosted.rs computes these refusals once per PyPI takeover purl, before any revert. A refused purl is never dispatched, so the dry run reports the same refusal as the wet run, and the package keeps its wiring, ledger entry and wheel.
  • CLI_CONTRACT.md: documents the new code and the Poetry takeover refusal.

The npm/PyPI/gem wrappers only dispatch to the binary, so they need no change.

Also in this PR

Per-issue checklist

New mode_migration_pypi e2e tests (they run in plain cargo test). Each test runs a real vendor and then a hosted scan:

Issue Test Without fix (bb3a25d) With fix (8be3951)
#723 direct six>=1.15 uv_pinned_down_direct_takeover_is_refused_before_revert FAILED (redirected: 0, redirect_takeover_unpatched) ok
#723 direct, dry run dry_run_predicts_uv_pinned_down_direct_takeover_refusal FAILED (redirected: 1) ok
#723 transitive (dateutil → six) uv_pinned_down_transitive_takeover_is_refused_before_revert FAILED (redirect_takeover_unpatched) ok
#723 transitive, dry run dry_run_predicts_uv_pinned_down_transitive_takeover_refusal FAILED (redirected: 1) ok
#945 Poetry 0.12 poetry_0_lock_takeover_is_refused_before_revert FAILED (redirect_takeover_unpatched) ok
#945 dry run dry_run_predicts_poetry_0_lock_takeover_refusal FAILED (redirected: 1) ok

Core unit tests in pypi_takeover::tests (6 passed): a pinned-down uv entry is refused, a uv entry at the patch version is admitted, an unparseable original is admitted (the revert's own drift handling owns that case), a Poetry 0 lock is refused, a Poetry 2.1 lock is admitted, and other flavors are admitted.

Local verification

  • cargo test -p socket-patch-cli --all-features --test mode_migration_pypi: 33 passed. That includes the controls uv_vendored_to_hosted and poetry_vendored_to_hosted (where the lock already resolves the patch version) and the existing requirements refusals.
  • cargo test -p socket-patch-cli --all-features --test coverage_fix_scan_hosted_dryrun_vendored --test covgap_commands_scan_hosted: 9 + 52 passed.
  • cargo clippy --workspace --all-features -- -D warnings: clean.
  • cargo test -p socket-patch-core --all-features --lib: everything passes except 5 tests that fail the same way on main in this sandbox:
    • utils::digest::tests::production_digests_go_through_the_helpers: the Gradle inline-hash guard that Route Gradle digests through utils::digest #878 fixes.
    • Four chmod-based write-failure tests (copy_tree relax loop, vlt_heal unremovable lock, poetry and requirements wire-failure rollback): they can't fail a write when run as root (uid 0 here).
  • cargo test --workspace --all-features couldn't finish locally because the sandbox ran out of disk (about 21 GB of test binaries). CI ran the full set on 0874e9d: all 547 check runs passed or were skipped, 0 failures.
  • cargo fmt: the new and changed hunks are rustfmt-clean. cargo fmt --all -- --check fails on main itself (466 pre-existing diffs; CI doesn't run it), so I didn't reformat unrelated files.

🤖 Generated with Claude Code


Note

Medium Risk
Changes hosted scan takeover ordering for PyPI projects; incorrect preflight could block valid migrations or still allow bad ones, but behavior is covered by new e2e and unit tests and only affects vendored→hosted transitions.

Overview
Vendored → hosted PyPI takeovers no longer revert wiring first when hosted mode cannot complete the redirect. A new preflight_pypi_takeover runs before any revert (wet and --dry-run), using the vendor ledger and on-disk locks.

For uv, takeover is refused with redirect_uv_takeover_version_unreachable when the recorded pre-vendor uv.lock unit version differs from the patch (revert would restore a version hosted mode will not pin). For Poetry, Poetry 0.x locks are refused up front with redirect_poetry_lock_unsupported instead of after un-vendoring. requirements.txt keeps the existing unreachable-pin check, now routed through the same preflight.

Hosted scan builds these refusals once per PyPI takeover candidate and skips dispatch when refused, so the package stays vendored (exit 0) instead of landing unpatched with redirect_takeover_unpatched. CLI_CONTRACT.md documents the uv code and Poetry takeover behavior; integration tests cover uv (direct/transitive) and Poetry 0.12 refusals.

Reviewed by Cursor Bugbot for commit 10c2237. Configure here.


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
Add regression tests for the vendored -> hosted takeover of a uv
project whose lock resolved another version than the patch (#723,
direct and transitive) and of a Poetry 0.12 lock (#945). Each case
runs wet and --dry-run and expects the takeover to be refused before
the revert, keeping the vendored patch. On main they fail: the wet
run leaves the package unpatched and the dry run reports a clean
takeover.

Assisted-by: Claude Code:claude-opus-5-5
`scan --mode hosted` over a vendored uv or Poetry project reverted the
vendored wiring first and only then asked the hosted rewriter to pin
the package. When the rewriter could not, the package ended up in
neither mode and the next install got the unpatched release, while
--dry-run promised a clean takeover.

The PyPI takeover gate now checks the hosted rewriter's reach before
the revert, wet and dry run alike, and keeps the package vendored:

- uv: vendored mode pins the lock entry down to the patch's version.
  If the recorded pre-vendor entry is at another version, the revert
  brings it back and hosted mode can't pin it. Refused with
  redirect_uv_takeover_version_unreachable (#723).
- Poetry: hosted mode refuses every Poetry 0.x lock. Refused with
  redirect_poetry_lock_unsupported (#945).

The requirements.txt check moves into the same core preflight.

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 6, 2026 15:47
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread crates/socket-patch-core/src/patch/redirect/pypi_takeover.rs
Comment thread crates/socket-patch-core/src/patch/redirect/pypi_takeover.rs
Port of #878. The digest guard test in socket-patch-core fails on
main because the Gradle cache crawler, jar comparator and Maven
sidecar hash inline. This keeps CI green on this branch and becomes
a no-op once #878 lands.

Assisted-by: Claude Code:claude-opus-5-5
The uv and Poetry takeover refusals told the user to re-lock while
the package was still vendored. That can't clear the uv refusal
(the recorded pre-vendor entry never changes) and makes the later
revert see drift. Both remedies now start with
`socket-patch vendor --revert`, say that it reverts every vendored
package, and only then re-lock and re-run hosted mode, matching the
requirements.txt refusal.

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[agent] coverage failed on 8be3951 in socket-patch-core --lib. The same check, plus test (macos-latest) and test (windows-latest), is red on main (9c43dfc). The cause is utils::digest::tests::production_digests_go_through_the_helpers: the Gradle cache crawler, jar comparator and Maven sidecar hash inline, and open PR #878 fixes that. I ported #878's three-file change here (d11b01c) so this PR can go green. It becomes a no-op once #878 merges.


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 6, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[burn-down agent] Ready for review at head 0874e9d.

  • CI: 547/547 check runs green or skipped on 0874e9d; no merge conflicts, 0 commits behind main.
  • Bugbot: reviewed 0874e9d, no new issues; no unresolved review threads.
  • Reviewer focus: the new preflight_pypi_takeover (pypi_takeover.rs) runs before any revert in scan/hosted.rs; check that the uv exact-version comparison matches the hosted planner's matching_package, and the new redirect_uv_takeover_version_unreachable code in CLI_CONTRACT.md.
  • Slack announcement not sent this run (no Slack send tool available); the next run will retry.

Generated by Claude Code

@mikolalysenko Mikola Lysenko (mikolalysenko) removed the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 7, 2026
Conflict in crates/socket-patch-cli/CLI_CONTRACT.md: both sides edited
the same one-line scan --mode hosted paragraph. Main added the
gem_mirror_overrides_source text; this branch added the uv/Poetry
takeover-refusal sentence. Kept main's paragraph and inserted this
branch's sentence at its original spot (after the requirements
takeover refusal). #963 on main covers the hosted -> vendored direction;
this branch's preflight covers vendored -> hosted, so both stay.

Co-Authored-By: Claude <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[burn-down agent] Merge commit 3a198b2 adds a stray file to the repo root. Its name is literally <<<<<<< HEAD...[12] and its only content is 12. It looks like a leftover from conflict resolution: it isn't on main, and it wasn't in the previous head 0874e9d. Please git rm it before this PR is labelled Ready for review. I'm leaving the PR to the agent that pushed 3a198b2, which still holds it. The branch also conflicts with main again.


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread crates/socket-patch-core/src/patch/redirect/pypi_takeover.rs Outdated
Comment thread &lt;&lt;&lt;&lt;&lt;&lt;&lt; HEAD...[12] Outdated
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wk9XBZsw4FpBuPb4DpKRPv
…-takeover-preflight

# Conflicts:
#	crates/socket-patch-cli/src/commands/scan/hosted.rs
#	crates/socket-patch-cli/tests/mode_migration_pypi.rs
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[agent] Removed the stray <<<<<<< HEAD...[12] file that merge 3a198b2 added (a6f28dc). Merged main again (2bf2333). The hosted.rs conflict with #984 is resolved so that the platform-wheel refusal (#701) runs first and the uv/Poetry/requirements preflight runs after it. The test helpers are combined as assert_takeover_refused_serving. Locally, mode_migration_pypi (37), pypi_takeover (6) and the hosted CLI suites (61) pass, and clippy is clean. Compared with main, the PR again changes only its 5 files.

BugBot review


Generated by Claude Code

Ledger purls keep the API's percent-encoding, so a PEP 440 local
version (+) arrives as %2B and never equals the lock's version,
refusing a reachable takeover. Decode it as matching_package does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wk9XBZsw4FpBuPb4DpKRPv
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Port of #1016. main routes the Gradle cache crawler, jar comparator
and Maven sidecar through utils::digest but still lists them as
pending, so production_digests_go_through_the_helpers fails on main
and every branch off it. No-op once #1016 lands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wk9XBZsw4FpBuPb4DpKRPv
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[agent] coverage failed on b9350a8 in socket-patch-core --lib, on utils::digest::tests::production_digests_go_through_the_helpers. It fails the same way on main (db83f01), where coverage and test (macos/windows) are red too. main routes the Gradle cache crawler, jar comparator and Maven sidecar through utils::digest but still lists them as pending. #1016 fixes this, and I copied its 3-line change here in 97d152c. It becomes a no-op once #1016 merges. The earlier #878 port is now identical to main (#878 was closed), so it contributes nothing to this PR's diff.


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

…-takeover-preflight

# Conflicts:
#	crates/socket-patch-cli/CLI_CONTRACT.md
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[agent] matrix (ubuntu-latest, 2022.12.19 2026.8.0, crlf marker-excluded extras category) failed on 10c2237. Only Pipenv 2022.12.19 crlf in hosted mode failed, on its lockOnlyRescanGreen check: the second lock-only scan --mode hosted against the live patch API was not green. Every other case in the job passed.

I don't think this PR caused it:

  • The same check passed on 85aa5a0, which has identical Rust code.
  • It also passed on main 05ecc6e. 10c2237 is just those two merged; the only changes since 85aa5a0 are to .github/workflows/ci.yml and scripts/tests/test_ci_e2e_tiers.py.
  • The PR's diff doesn't touch Pipenv lock rewriting. It only adds the PyPI vendored→hosted takeover check (pypi_takeover.rs).

I couldn't reproduce it locally because the sandbox can't reach patches-api.socket.dev. No fix exists to port. I'm re-running the failed job once; if it fails again, I'll treat it as a real failure.


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 10c2237. Configure here.

@mikolalysenko
Mikola Lysenko (mikolalysenko) added this pull request to the merge queue Oct 7, 2026
@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 7, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[agent] Ready for review at 10c2237.

  • Changes this pass: merged origin/main (05ecc6e, which adds the merge-queue ci-ok job). It merged cleanly and only touched CI files: ci.yml and scripts/tests/test_ci_e2e_tiers.py, whose 19 tests pass locally. No code changes.
  • CI on head: 557 success, 6 skipped, 0 failing. ci-ok is green and mergeable_state is clean.
    • Pipenv 2022.12.19 crlf hosted failed once on lockOnlyRescanGreen. The same cell passed on the previous head with identical Rust code, so I treated it as a flake. One rerun passed.
    • 4 Bun macOS native jobs never got a runner and were cancelled after about 4 hours in the queue. One rerun passed.
  • Bugbot: ran on 10c2237 and passed with no new findings. All 4 earlier Bugbot threads are resolved: uv remedy, Poetry remedy, PURL percent-decode, and the merge-conflict leftover. I checked that the percent-decode fix is in pypi_takeover.rs and that no stray conflict file is tracked.

Generated by Claude Code

Merged via the queue into main with commit c8f1356 Oct 8, 2026
614 of 619 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the agent/fix-pypi-hosted-takeover-preflight branch October 8, 2026 00:33
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 8, 2026
Resolve conflicts with main's #946 (PyPI takeover pre-gate), #1042
(containment helper) and #1077 (classic berry-migration warning):

- hosted.rs: keep this PR's staged takeover; main's new
  `preflight_pypi_takeover` pre-gate inside the deleted
  `vendored_takeover` is dropped. The staged takeover's `explain` now
  calls `preflight_pypi_takeover` (instead of only the requirements
  check), so a retracted uv pin-down (#723) or Poetry 0.x (#945)
  takeover is still skipped with `redirect_uv_takeover_version_unreachable`
  / `redirect_poetry_lock_unsupported`, as main's tests expect.
- socket_dir.rs: use main's `containment::ensure_unlinked` guard, then
  this PR's deferred removal.
- redirect/mod.rs: take main's `pinned_any` (a mirror-refused entry
  counts only when it already carries our hosted pin), which subsumes
  this PR's mirror fix.
- CLI_CONTRACT.md: describe the uv/Poetry cases as retractions.
- mode_migration_pypi.rs: keep both sides' tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review

Projects

None yet

3 participants