Skip to content

After pipenv uninstall of a vendored package from a named category on Pipenv 2022/2023, scan --prune, vendor --revert, remove and rollback keep it as "drifted", so vendor --check stays red and its scan --prune remedy loops #1142

Description

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

Summary

A package is vendored in a Pipenv named category (for example [docs]), and it's the only package in that category. When the user runs pipenv uninstall six --categories docs, Pipenv 2022.x and 2023.x drop the now-empty docs key from Pipfile.lock entirely. Pipenv 2026 keeps "docs": {}.

On main, the vendored Pipenv revert reads a missing category key as drift, not as removal. So after the uninstall:

  • vendor --check exits 1. It says "dependency removed … run socket-patch scan --mode vendored --prune to revert the vendored entry".
  • scan --mode vendored --prune exits 0 but lists the entry under gc.keptVendoredEntries. The artifact and the ledger entry stay.
  • vendor --check is still red afterwards.
  • vendor --revert exits 0 and keeps the entry: "undo the drift (restore the vendored lock entries or re-vendor) and re-run vendor --revert".
  • remove pkg:pypi/six@1.16.0 and rollback pkg:pypi/six@1.16.0 exit 1: "re-run scan --mode vendored to normalize, then remove again". A re-scan is a no-op, because six is no longer in the project.

The user can't "undo the drift" without reinstalling the package they just removed. The only way to get vendor --check green again is to delete .socket/vendor/state.json entries and the artifact by hand.

Impact

Once a dependency is uninstalled this way, a CI gate on vendor --check stays red permanently. Every remedy the CLI prints is a no-op or loops. The dead wheel and ledger entry are never reclaimed. Nothing installs unpatched bytes, so this isn't a security issue, but the CI gate is broken.

Expected vs actual

  • Expected: the revert already retires a record whose entry a relock dropped from a category that still exists. It returns vendor_lock_entry_relocked and says so in its comment: "A relock dropped the entry (pipenv uninstall <pkg>, a Pipfile edit + pipenv lock) … retire the record rather than keep the orphan forever" (crates/socket-patch-core/src/vendor/pypi_pipenv.rs:616-626). docs/usage.md:223 says --prune removes "records for dependencies that left the project". vendor --check itself sends the user to scan --prune. When the uninstall also removed the category key, the record should be retired in the same way.
  • Actual: with the whole category gone, the record is treated as drift and kept forever.

Repro (Linux, main 823810a, real Pipenv 2023.12.1 on py3.11, local mock patch API serving a patched six 1.16.0 wheel)

mkdir p && cd p && git init -q
cat > Pipfile <<'EOF'
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
idna = "==3.7"

[docs]
six = "==1.16.0"

[requires]
python_version = "3.11"
EOF
pipenv lock
socket-patch scan --mode vendored --yes $MOCK_ARGS      # docs:six -> ./.socket/vendor/pypi/<uuid>/six-…whl
git add -A && git commit -qm vendored
pipenv uninstall six --categories docs                  # Pipfile keeps an empty [docs]; Pipfile.lock loses the "docs" key
socket-patch vendor --check; echo $?                    # 1: "dependency removed … run `scan --mode vendored --prune`"
socket-patch scan --mode vendored --yes --prune $MOCK_ARGS --json | jq .gc.keptVendoredEntries   # ["pkg:pypi/six@1.16.0"]
socket-patch vendor --check; echo $?                    # still 1
socket-patch vendor --revert                            # "Pipfile.lock entry for Some(\"docs:six\") changed since vendoring; left untouched" … Kept 1 drifted package
socket-patch remove pkg:pypi/six@1.16.0 --yes; echo $?  # 1: drift-kept, "re-run scan --mode vendored to normalize"

The same shape fails if [docs] is edited by hand and then pipenv lock runs. Control: when six is removed from [docs] on Pipenv 2026.8.0 (which keeps "docs": {}), or from [packages] on any version (default always stays), scan --prune reverts the entry, the artifact is removed and vendor --check exits 0.

OS × version

OS Pipenv result
Linux 2022.12.19 fail (2/2 runs)
Linux 2023.12.1 fail (2/2 runs; remove / rollback also exit 1)
Linux 2026.8.0 pass (Pipenv keeps the empty docs key)
macOS / Windows — untested (lock-shape bug, OS-independent)

First bad: not in v4.0.0, which has no vendor --check, and whose scan --prune never judged Pipenv entries. On main, #1050 routes the prune GC and the check's "dependency removed" remedy through Discovery::vendor_entry_in_use. That verdict correctly returns Some(false) here, but the revert then drift-keeps the entry.

Suspect code

crates/socket-patch-core/src/vendor/pypi_pipenv.rs:600-602 (revert_pipenv): let Some(map) = lock.get_mut(section)… else { warnings.push(drifted()); continue; }. A missing category is reported as vendor_lock_entry_drifted. The missing-entry arm a few lines below (:616-626) retires a Rewritten record that has an original. The missing-section case could take the same arm, perhaps only when the section isn't _meta and the lock still parses as a pipfile-spec 6 lock. The unit test revert_drift_skips_missing_section_entry_new_and_missing_original (:1804) currently pins the drift behaviour for a deleted section.

Related, but in other backends with separate code: #1132 (bun.lockb) and #1140 (uv).

Probe runs: none. This is a lock-shape bug that doesn't depend on the OS, and probe branches are currently blocked for this routine.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions