Skip to content

Fix uv script-lock revert per pair (#1285, #890) - #1391

Merged
Mikola Lysenko (mikolalysenko) merged 5 commits into
mainfrom
agent/fix-pypi-lock-revert-per-pair
Oct 11, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 5 commits into
mainfrom
agent/fix-pypi-lock-revert-per-pair

Conversation

@mikolalysenko

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

Copy link
Copy Markdown
Collaborator

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

Fixes #1285
Fixes #890

Summary

A vendored PyPI entry can wire several PEP 723 scripts (a.py +
a.py.lock, b.py + b.py.lock) or several PEP 751 locks
(pylock.toml, pylock.dev.toml) at once. Its revert now handles each
pair separately, so a change to one script no longer strands the others.
Every unwind benefits: vendor --revert, scan --prune, remove,
rollback, and the vendored → hosted takeover.

Root cause

revert_python_locks (crates/socket-patch-core/src/vendor/pypi_lock.rs)
decided liveness for the whole ledger entry at once:

Fix

Out of scope: #890 has a later comment saying a deleted uv.lock blocks
the project lane. That goes through a different backend
(pypi.rs revert_uv) and is a separate root cause, so it is not claimed
here.

Tests (red → green)

Per issue:

Local runs:

  • cargo fmt --all -- --check and cargo clippy --workspace --all-features -- -D warnings: clean.
  • cargo test --workspace --all-features --no-fail-fast: everything this
    PR touches passes. 13 tests fail in this sandbox and also fail on
    main here. 12 are permission-injection tests (*_write_failure_*,
    *unremovable*, relax_loop_must_not_traverse_symlinked_root) that
    can't fail a write when run as root. The 13th,
    e2e_bun_lockb::binary_shared_bundled_record_hosted_pin_is_managed,
    fails on an npm missing_blob download; I re-ran it on main with the
    same result.
  • cargo test -p socket-patch-cli --all-features --test e2e_vendor_pypi_build -- --include-ignored (uv 0.11.32): 52 passed.
  • No wrapper changes (npm/, pypi/, gem/): the revert logic is only in the Rust core.

/code-review high ran on the diff. Its findings are fixed in 194f0c3:
one read per file (closes a delete-mid-revert race), a deduplicated
warning in wording that is also true on a dry or drift-kept run,
script_of_lock reused, and the takeover forwarding the advisory.

Bugbot (194f0c3) found that the missing-file probe read the disk, not the
GroupCommit overlay the takeover reverts inside. Fixed in e7fae29, with
the regression test missing_is_judged_through_the_group_commit_overlay
(red before the fix, green after).

Human review (e7fae29) found that a renamed vendored script (git mv a.py tool.py) would lose its wheel, because the residual-reference guard never read
root *.py files. Fixed in b8bd9e6, with the test
mode_migration_pypi::renamed_vendored_script_keeps_its_wheel (red before
the fix, green after).

🤖 Generated with Claude Code

https://claude.ai/code/session_01LSV2D8AnkmcqFDijhzZGhE


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
A vendored PyPI entry can wire several PEP 723 scripts (a.py and
b.py, each with its .py.lock) or several PEP 751 locks at once. The
revert judged the whole entry together, so a change to one script
stranded all of them:

- After `uv remove --script b.py six`, a.py still named the vendored
  wheel, so b.py's removal counted as drift. vendor --revert left
  a.py wired, and remove and rollback exited 1 (#1285).
- Deleting b.py, b.py.lock or pylock.dev.toml failed every unwind
  with "cannot read ./b.py.lock", and the hosted takeover pointed at
  the same failing command (#890).

The revert now decides per pair: a pair that no longer names the
wheel is removed, not drift, and a wired file that no longer exists
warns vendor_lockfile_missing and is skipped while the remaining
files are restored. Other read errors still fail closed, and a hand
edit inside a still-wired pair is still drift.

Fixes #1285
Fixes #890

Assisted-by: Claude Code:claude-opus-5-5
Review follow-ups to the per-pair revert:

- Read every wired file once and judge every record against that
  text, so a file deleted mid-revert cannot fail it with
  "cannot read".
- Warn vendor_lockfile_missing once per file, in wording that holds
  on a dry run or a drift-kept revert.
- Pair a .py.lock with its script through script_of_lock.
- The hosted takeover now passes the missing-file advisory through,
  so scan --mode hosted names a deleted script too.

Assisted-by: Claude Code:claude-opus-5-5
@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/vendor/pypi_lock.rs
The hosted takeover reverts inside a group commit, whose in-memory
overlay can hold a removal the disk does not show yet. The missing
check looked at the disk while the read went through the overlay, so
a file the run had removed still failed the revert with "cannot
read". The check now uses the overlay-aware read and treats its
NotFound as missing.

Assisted-by: Claude Code:claude-opus-5-5
@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 Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 11, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Ready for review at e7fae29.

  • CI: 35/35 non-skipped checks green on the head (18 skipped by path filters); mergeable, no conflicts; no main-wide failures.
  • Bugbot: reviewed e7fae29 (check passed); its one thread on pypi_lock.rs:720 is resolved.
  • Reviewer focus: revert_python_locks in crates/socket-patch-core/src/vendor/pypi_lock.rs now decides liveness per script/lock pair; check the drift handling for a pair whose file changed while siblings stay untouched, and the matching CLI_CONTRACT.md wording.

Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Final review brief

What it does. revert_python_locks now reads every wired file once, up front, and decides the "removed, not drift" rule from #1214 separately for each pair (a script plus its .py.lock, or one pylock*.toml) instead of for the whole ledger entry. So after uv remove --script b.py, the untouched a.py is still restored (#1285). A deleted wired file now gives one vendor_lockfile_missing warning, and the files that remain are restored, instead of every unwind failing (#890). The vendored → hosted takeover now passes that warning through, and CLI_CONTRACT.md documents it.

Risk: low–medium. The change is confined to one revert function, and any read error other than NotFound still fails closed. One intended behaviour change: an entry whose only wired file was deleted used to fail; now it succeeds with a warning, which matches the bun and gem reverts.

Look here

Verified. Read the full diff. cargo test -p socket-patch-core --lib pypi_lock: 60 passed. mode_migration_pypi: 55 passed. --lib takeover: 12 passed. in_process_vendor_bun_takeover: 40 passed. Red check: with main's pypi_lock.rs logic and the PR's tests, 4 of the 6 new core tests fail; the other 2 are guard tests (drift in a still-wired pair; unreadable file fails closed) and pass on both, as they should. I checked these edge cases and found them correct: only the script deleted, only the lock deleted, both deleted, dry runs, and a hand edit inside a live pair (still drift). Windows paths: wiring entries are bare filenames (allowed_file), and there is no CRLF-specific logic. Each file is now read once instead of twice. CI 53/53 green (35 pass, 18 skipped) on e7fae29; Cursor Bugbot: no new issues. CHANGELOG untouched; no debug leftovers.

Changes I made. None.

Open questions (non-blocking).

  • The takeover filter forwards every vendor_lockfile_missing, not only pypi's. Bun's revert also emits that code when bun.lock is missing, so a bun takeover will now show it in warnings[] too. That is probably desirable, but it is unstated and not covered by a test.
  • The vendor_lockfile_missing row in the CLI_CONTRACT.md error-code table still lists only the bun preflight placement; the prose covers the new placement. The code is also a bare string literal in two files, where a shared constant would be safer.

Auto-merge is armed: approving sends it straight to the merge queue.


Generated by Claude Code

Comment thread crates/socket-patch-core/src/vendor/pypi_lock.rs
The revert now skips a wired script that no longer exists. When the
user renamed it (`git mv job.py tool.py`) rather than deleting it,
tool.py still installs from the vendored wheel through its own
[tool.uv.sources] path. But the residual-reference guard never read
root *.py files, so the revert deleted that wheel and broke
`uv run tool.py`.

The guard now also reads root *.py files. uv resolves a script's
source paths from the script's own directory, so only a root script
can still reach .socket/vendor/. A renamed script now keeps the wheel
with vendor_revert_residual_reference, naming the file.

Assisted-by: Claude Code:claude-opus-5-5
@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.

✅ 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 b8bd9e6. Configure here.

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Final review brief (update for b8bd9e6)

What it does. Same change as the earlier brief: the uv script-lock revert decides "removed vs. drift" per script/lock pair (#1285), and a deleted wired file warns vendor_lockfile_missing instead of failing the unwind (#890). One commit since that brief, b8bd9e6: if a vendored script was renamed (git mv job.py tool.py), the revert used to delete the wheel the renamed script still installs from. The residual-reference guard now also reads root *.py files, so the wheel is kept and vendor_revert_residual_reference names the file.

Risk: low–medium (unchanged). The new commit only widens a fail-closed guard. The worst case is that the artifact is kept when a root .py mentions the uuid dir for some other reason, and the warning names that file.

Look here

  • pypi.rs#L2296-L2302: root *.py added to the probe set
  • pypi.rs#L2711-L2722: the post-revert guard that now catches the renamed script (it runs after wired files are restored, so restored scripts don't trip it)
  • mode_migration_pypi.rs#L1208-L1243:`` renamed_vendored_script_keeps_its_wheel covering `vendor --revert`, `remove` and `rollback`
  • Earlier brief's links still cover the main change in pypi_lock.rs.

Verified. Read the delta. mode_migration_pypi: 56/56 pass. Red check: dropping the .py clause makes renamed_vendored_script_keeps_its_wheel fail. socket-patch-core --lib pypi: 527 pass. The 2 failures are chmod-based write-failure tests in pypi_poetry/pypi_requirements. They fail only because my sandbox runs as root, they're untouched by this PR, and they're green in CI. The other callers (guard_unwired_pypi_revert and the dry-run preview) both still fail closed, which is correct. CI 35 pass, 19 skipped, 0 failed on b8bd9e6. Cursor Bugbot: success. Review threads: all resolved. CHANGELOG untouched.

Changes I made. None. I disarmed auto-merge when the new commit landed and re-armed it after this review.

Open questions (non-blocking). The two from the earlier brief still apply (takeover forwards every vendor_lockfile_missing; the CLI_CONTRACT error-code table row).

Auto-merge is armed: approving sends it straight to the merge queue.


Generated by Claude Code

Merged via the queue into main with commit 6f0ea2c Oct 11, 2026
54 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the agent/fix-pypi-lock-revert-per-pair branch October 11, 2026 20:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment