Skip to content

Fix -g runs unpatching vendored NuGet (#489) - #1790

Merged
Mikola Lysenko (mikolalysenko) merged 5 commits into
mainfrom
agent/fix-nuget-global-folder-ownership
Oct 28, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 5 commits into
mainfrom
agent/fix-nuget-global-folder-ownership

Conversation

@mikolalysenko

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

Copy link
Copy Markdown
Collaborator

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

Fixes #489

Summary

A global run (-g / --global-prefix) started inside a vendored NuGet project
no longer takes over, or unpatches, the project's own installed copy.

Root cause

project_state_in_scope treats a global run's target as separate from the
cwd project's install. For NuGet that's wrong: a PackageReference project
restores into the global packages folder (~/.nuget/packages or
NUGET_PACKAGES), so the "global copy" of a package the project vendors is
the project's patched install. Since #446:

  • scan -g --mode agent and apply -g took that copy over as an agent patch;
  • rollback -g and remove -g restored upstream bytes into it.

dotnet restore --locked-mode never re-extracts it, because the lock and the
nupkg's sha512 still match. So the project built unpatched with exit 0
everywhere while vex kept attesting not_affected.

Fix

  • NuGetCrawler::global_roots_shared_with (core) says whether the global run's
    NuGet folder is one the project restores into. It uses the packageFolders
    its restores recorded, plus the global folder while some project has no
    restore yet. It needs no root-level .NET marker.
  • commands::vendor_owned_keys (CLI) is the single ownership rule. In project
    scope the whole ledger owns. Under global scope only the ledger's NuGet keys
    own, and only when that folder is shared and --ecosystems selects nuget.
  • scan, apply (both paths, including --check), rollback and remove use it:
    • scan skips the copy as vendored and adds vendored_ownership_retained;
    • apply skips it as vendored;
    • rollback and remove leave it patched and report a skipped / vendored
      event plus a human [skip] line;
    • rollback keeps its manifest record.
  • Every other ecosystem, and any NuGet store the project doesn't restore into
    (such as RestorePackagesPath), keeps Fix -g touching the cwd project's state (#436, #445) #446's behavior.
  • CLI_CONTRACT.md documents the exception.

Out of scope

The hosted variant (from a comment on #489) predates #446. Agent mode
doesn't yield to hosted pins even in project scope, so fixing it needs a
contract decision on a new skip reason. I split it into #1793.

Test evidence

Red on main, green on this branch:

Issue / leg Test main branch
#489 rollback -g, --global-prefix global_scope_project_state::global_rollback_keeps_a_vendored_nuget_projects_install FAIL ok
#489 remove -g global_remove_keeps_a_vendored_nuget_projects_install FAIL ok
#489 apply -g global_apply_skips_a_vendored_nuget_projects_install FAIL ok
#489 scan -g --mode agent global_agent_scan_skips_a_vendored_nuget_projects_install FAIL ok
#489 full repro, real .NET SDK 8.0.132 + nuget.org e2e_nuget_dotnet_build::nuget_vendored_project_survives_global_agent_scan_and_rollback FAIL ok
#446 kept: a store the project doesn't use global_rollback_restores_a_store_the_nuget_project_does_not_use ok ok
relocated restore / markerless repo nuget_crawler::tests::global_roots_shared_with_* (3) n/a ok

Commands run locally:

  • cargo fmt --all -- --check: ok.
  • cargo clippy --workspace --all-features -- -D warnings: ok.
  • cargo test -p socket-patch-cli --all-features --test global_scope_project_state: 47 passed.
  • SOCKET_PATCH_DOTNET_E2E_REQUIRED=1 SOCKET_PATCH_DOTNET_E2E_VERSION=8 cargo test -p socket-patch-cli --all-features --test e2e_nuget_dotnet_build -- --ignored: 4 passed.
  • cargo test -p socket-patch-cli --all-features --test e2e_nuget -- --include-ignored: 22 passed.
  • cargo test --workspace --all-features --no-fail-fast: 11 failures, all outside this change.
    • They are identical before and after this branch's review fixes.
    • The sandbox runs as root, so the chmod-based write-failure tests can't fail as intended (in_process_redirect ×3, repair ×2, core lib ×4).
    • The e2e_bun_lockb ×2 tests need patches-api.socket.dev and a pinned bun.
    • CI is green on every head of this PR.

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

/code-review high findings not fixed

  • Corrupt ledger under -g fails open. A corrupt .socket/vendor/state.json
    yields no keys, so the old -g behavior returns. Failing closed would refuse
    every -g run (npm included) started in a project with a corrupt ledger,
    and project-scope commands already report the corruption. Not worth the
    blast radius.
  • remove -g reads the ledger once more. One small extra file read on a
    global run, which is otherwise never locked against the vendor flow; it isn't
    a correctness issue.

https://claude.ai/code/session_011f9aeT3Qr5oee9inqu1TVJ


Note

Medium Risk
Changes global-scope ownership and rollback/remove/apply behavior for NuGet when cwd is a vendored project; wrong folder detection could still patch or restore the wrong copy, but logic is scoped to NuGet ledger keys and restore-recorded folders.

Overview
Global -g runs no longer unpatch or take over a vendored NuGet project's own install when that install lives in the global packages folder the project restores into (NUGET_PACKAGES / default ~/.nuget/packages). Previously those commands treated that copy as a separate global target, which could restore upstream bytes that dotnet restore --locked-mode never refreshed—leaving the project building unpatched while tooling still looked healthy.

The fix adds NuGetCrawler::global_roots_shared_with to detect overlap between the global run's NuGet roots and folders recorded in the project's restores (project.assets.json packageFolders, with sensible fallbacks), and vendor_owned_keys as the single rule for which ledger keys still own copies under global scope (NuGet keys only, when shared and NuGet is in scope). scan, apply, rollback, and remove now use it: agent global flows skip as vendored / vendored_ownership_retained; rollback/remove leave the copy patched, emit skipped / vendored events and human [skip] lines, and rollback keeps manifest records. Unrelated global stores (e.g. RestorePackagesPath) and other ecosystems keep prior #446 behavior. CLI_CONTRACT.md documents the NuGet exception.

Tests cover in-process global-scope scenarios, NuGet crawler sharing logic, and a real .NET SDK e2e repro.

Reviewed by Cursor Bugbot for commit 8c4fb08. Configure here.


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
A NuGet PackageReference project restores into the global packages
folder, so a global run's copy of a package the project vendors is the
project's own patched install. Since #446, `scan -g --mode agent` and
`apply -g` took it over as an agent patch, and `rollback -g` or
`remove -g` restored upstream bytes into it. A locked restore never
re-extracts it, so the project built unpatched with exit 0 while vex
kept attesting.

Global runs now leave that copy to the project's vendor ledger when
the global folder is one the project restores into: scan skips it as
vendored_ownership_retained, apply skips it as vendored, and rollback
and remove keep it patched. A global store the project doesn't use is
still patched and restored as before.

Fixes #489

Assisted-by: Claude Code:claude-opus-5-5
Describe in CLI_CONTRACT.md the one store a global run leaves to the
project: a NuGet project's own install in the global packages folder
it restores into. Add a real-dotnet capstone: vendor, locked restore,
scan -g --mode agent, rollback -g, then a second locked restore must
still extract the patched bytes.

Refs #489

Assisted-by: Claude Code:claude-opus-5-5
With the fix, scan -g no longer records the project's install, so the
rollback leg had no manifest to read. Seed the agent record an older
scan -g wrote, so the test proves rollback -g leaves the install
patched for users upgrading from an affected version.

Refs #489

Assisted-by: Claude Code:claude-opus-5-5
Review follow-ups for the global-scope NuGet ownership check:

- A project that restores into RestorePackagesPath never reads the
  global packages folder, so its global copies stay the global run's.
  The check now uses the folders the project's restores recorded, and
  the global folder only while a project has no restore yet.
- Repositories whose projects all live in subdirectories, with no
  root .NET marker, are covered too.
- rollback -g and remove -g now report each copy they leave to the
  project as a skipped/vendored event and a human [skip] line,
  instead of skipping it silently.
- Runs whose --ecosystems excludes nuget skip the check, and path
  canonicalization moves onto the blocking pool.

Refs #489

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 8c4fb08. Configure here.

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

Copy link
Copy Markdown
Collaborator Author

Ready for review at 8c4fb086 (burn-down agent).

  • CI: 82/82 (32 success, 50 skipped) check runs green or skipped on 8c4fb086; no main-wide failures.
  • Bugbot: reviewed 8c4fb086, no findings; no open review threads.
  • Mergeable, no CHANGELOG.md change.
  • Slack announcement not sent this run (Slack send unavailable); the next run retries.

Generated by Claude Code

Merged via the queue into main with commit 86457da Oct 28, 2026
82 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the agent/fix-nuget-global-folder-ownership branch October 28, 2026 21:56
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