Skip to content

Agent-mode rollback in the other scope (rollback -g after a project apply, or rollback after a -g apply) deletes the manifest entry and blobs while the patched copy stays patched, and exits 0 #450

Description

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

Summary

The agent-mode manifest (.socket/manifest.json) doesn't record which scope a patch was applied in. It could be the project (a Poetry venv) or a global install (-g / --global-prefix). rollback only crawls its own scope. When the copy it finds there is already original, it counts the patch as rolled back, removes the manifest entry and garbage-collects its blobs. The copy that is actually patched, in the other scope, is left patched, and the run exits 0 with status: success.

It happens in both directions, from inside a Poetry project:

Applied with Rolled back with Result
scan --mode agent (patches the project's .venv) rollback -g rolledBack: 0, alreadyOriginal: 1 (the global copy), manifest.removedEntries: ["pkg:pypi/six@1.16.0"], exit 0. The .venv stays patched.
scan -g --mode agent (patches ~/.local/lib/python3.11/site-packages) rollback rolledBack: 0, alreadyOriginal: 1 (the .venv copy), removedEntries: ["pkg:pypi/six@1.16.0"], gc.removedBlobs: 1, exit 0. The global copy stays patched.

Once the entry is gone, the right-scope rollback has nothing to do. In the second case, a later rollback -g reports rolledBack: 0, alreadyOriginal: 0 and exits 0, with the global copy still patched.

Impact

  • The user is told the rollback succeeded, but a patched copy stays in place, and the only local record of it (manifest entry plus before-blobs) is deleted. Nothing on the machine can find or revert it any more, short of re-applying the same patch and rolling back in the right scope.
  • This is easy to hit with Poetry. The same packages are commonly in the project venv and in the user site or system interpreter (the same six, requests or urllib3), and -g from inside a project directory shares the project's .socket/manifest.json.
  • It isn't specific to Poetry or pypi. The logic is ecosystem-agnostic (see the suspect code). It was reproduced here with a real Poetry venv and a real pip install --user global copy.

Repro (Linux, Poetry 2.1.1, Python 3.11)

The mock patch API serves one agent patch for pkg:pypi/six@1.16.0: it appends # SOCKET-PATCHED to six.py, using the same routes as tests/vex_pypi_real_common/mod.rs plus /v0/orgs/<org>/patches/blob/<hash>. $A = --api-url http://127.0.0.1:18766 --api-token fake --org test-org --patch-server-url http://127.0.0.1:18766.

python3 -m pip install --user --ignore-installed six==1.16.0     # the global copy
U=~/.local/lib/python3.11/site-packages
mkdir proj && cd proj
cat > pyproject.toml <<'EOF'
[tool.poetry]
name = "gproj"
version = "0.1.0"
description = ""
authors = ["x <x@x>"]
package-mode = false
[tool.poetry.dependencies]
python = "^3.11"
six = "1.16.0"
EOF
poetry config virtualenvs.in-project true --local && poetry install
V=.venv/lib/python3.11/site-packages

# Direction 1: project apply, global rollback
socket-patch scan --mode agent --yes --json --ecosystems pypi $A    # action "added"; $V/six.py patched
socket-patch rollback -g --json --ecosystems pypi $A                # exit 0, alreadyOriginal 1, removedEntries [six]
tail -1 $V/six.py                 # SOCKET_PATCHED = 1   <- still patched
cat .socket/manifest.json         # {"patches": {}}

# Direction 2: global apply, project rollback (restore both six.py files and rm -rf .socket first)
socket-patch scan -g --mode agent --yes --json --ecosystems pypi $A # $U/six.py patched
socket-patch rollback --json --ecosystems pypi $A                   # exit 0, alreadyOriginal 1, removedEntries [six], gc.removedBlobs 1
tail -1 $U/six.py                 # SOCKET_PATCHED = 1   <- still patched
socket-patch rollback -g --json --ecosystems pypi $A                # exit 0, rolledBack 0: nothing left to revert

Expected vs actual

  • Expected: --global means "Operate on globally-installed packages" (CLI_CONTRACT.md, Global arguments), so rollback -g should affect global copies and the records that belong to them, and a project rollback should affect the project. The contract treats a copy that is already original / not installed as satisfying rollback's end state, because "rollback's job is 'make the tree unpatched'" (CLI_CONTRACT.md, JSON migration notes for rollback). Here the tree is not unpatched: the record's patched copy just sits outside the scope that was crawled. The entry should be kept (and its blobs pinned, as the crawler-miss guard already does for not_installed) unless every copy it was applied to is verified original. Alternatively, rollback could warn and exit non-zero.
  • Actual: an already_original copy in the current scope counts as success. The purl lands in succeeded_purls and is removed from the manifest, and GC sweeps the before-blobs (the crawler-miss pin covers only not_installed).

OS × version matrix

OS Poetry main 2463257 PR #446 head 92c71ad release 4.0.0
Linux 2.1.1 repro (both directions, 2/2 each) repro (both directions, 2/2 each) not reproduced (entry kept, both directions, 2/2)

macOS and Windows weren't run: this routine can't create probe branches this run. The logic has no OS-specific path.

First bad commit

d5e1815 (#231, "full-state rollback default"). Both directions, 2/2 runs each, on Linux with Poetry 2.1.1:

Build Project apply + rollback -g -g apply + rollback
release 4.0.0 (v4.0.0) entry kept entry kept
d5e1815 (#231) entry removed, .venv still patched entry removed, global still patched
main 2463257 entry removed entry removed

The other commits between v4.0.0 and d5e1815 (#230, #232, #233) don't touch rollback.

Suspect code

  • crates/socket-patch-cli/src/commands/rollback.rs:1574-1597: succeeded_purls takes every r.success result, including results where every file is already_original. A purl in it is removable whatever scope its copies were patched in.
  • crates/socket-patch-cli/src/commands/rollback.rs:1641-1650: the GC pin covers removed not_installed purls only, so the before-blobs of the dropped entry are swept.
  • The manifest records no scope or install path for an entry, so rollback can't tell a copy that was never patched in this scope from a reverted one.

Related

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

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions