Skip to content

Fix NuGet patching the default cache over configured folders (#397, #398) - #1769

Merged
Mikola Lysenko (mikolalysenko) merged 5 commits into
mainfrom
agent/fix-nuget-configured-package-folders
Oct 28, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 5 commits into
mainfrom
agent/fix-nuget-configured-package-folders

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 #397
Fixes #398

Summary

Agent-mode apply, scan and vex on NuGet now use the package folder the project actually restores from. Before this change, a project that relocated its packages folder had the user-wide ~/.nuget/packages copy patched instead. apply exited 0 and VEX attested not_affected while the build linked the unpatched copy.

Root cause

nuget_crawler::package_roots listed roots in this order: <cwd>/packages, then the default global folder (NUGET_PACKAGES or ~/.nuget/packages), then the packageFolders of obj/project.assets.json (root and one level down only). The first root that yields a purl wins (merge_first_wins). It never read the folders NuGet is configured to use:

Fix

  • formats::nuget::parse_config now records configuration/config/add settings (and <clear />). New effective_setting resolves a setting from the nearest config.
  • vendor::nuget_config::settings_chain reads the configs NuGet merges for a directory (that directory and every ancestor, nearest first, then the user config) once per crawl. A relative --cwd is made absolute first. SettingsChain::folder expands %VAR% the way .NET does and resolves relative values against the directory of the config that set them.
  • Local roots, in priority order:
    1. The packages.config folder: repositoryPath, honored only when a packages.config exists under the root; otherwise <cwd>/packages.
    2. The packageFolders of every obj/project.assets.json under the root, at any depth. These record RestorePackagesPath and a configured globalPackagesFolder as the restore resolved them. This reuses the existing restore-scope walk (restore_scope became restore_walk). The walk never enters the configured stores, or any store a restore names, so a big in-tree store can't use up its 10k-directory budget.
    3. The global folder NuGet would use: NUGET_PACKAGES, then the nearest globalPackagesFolder, then the default. A relocated folder replaces the default one.
  • Global mode (-g) uses the same global-folder rule over the user config.
  • The default folder honors DOTNET_CLI_HOME off Windows, matching the existing user-config lookup.

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

Tests (red on main → green here)

Issue Test Before fix
#397 local, warm default cache, src/App + globalPackagesFolder nuget_crawler::tests::configured_global_packages_folder_beats_a_warm_default_cache FAILED
#397 cold default cache, project two levels deep …::deep_project_restore_is_found_with_a_cold_default_cache FAILED
#397 RestorePackagesPath …::restore_packages_path_beats_a_warm_default_cache FAILED
#397 -g with user-config globalPackagesFolder …::global_mode_reads_the_user_config_global_packages_folder FAILED
#397 -g end to end (scan -g --sync patches the configured copy, leaves the default copy alone) in_process_remote_ecosystems_apply::nuget_global_apply_patches_the_user_configured_global_packages_folder FAILED
#398 repositoryPath=lib/pkgs, from the solution root and from the project dir …::repository_path_folder_beats_the_global_folder FAILED
Bugbot: walk entered a store its own restore names …::restore_walk_does_not_enter_a_store_its_restore_names FAILED on 01ff0ff
guard: NUGET_PACKAGES still overrides config …::nuget_packages_env_overrides_the_configured_folder ok (unchanged behavior)
guard: a shared repositoryPath without packages.config is ignored …::repository_path_is_ignored_without_a_packages_config new
config reader / env expansion / relative cwd formats::nuget::tests::config_settings_resolve_nearest_first, vendor::nuget_config::settings_tests::* new

Commands run locally (all pass):

  • cargo fmt --all -- --check
  • cargo clippy --locked --workspace --all-features -- -D warnings
  • cargo test -p socket-patch-core --all-features --lib: 4 failures are permission-based tests in files this PR doesn't touch (copy_tree, vlt_heal, pypi_poetry, pypi_requirements) that can't fail under uid 0 in this container.
  • cargo test -p socket-patch-core --all-features --test crawler_nuget_e2e
  • cargo test -p socket-patch-cli --all-features --lib --test in_process_remote_ecosystems_apply --test e2e_nuget --test ecosystem_dispatch_e2e --test e2e_vex --test e2e_vex_vendor --test e2e_vex_redirect --test in_process_rollback_all_ecosystems --test in_process_get_hosted_ecosystems --test e2e_safety_internals

CI on b90c942 is green (ci-ok success, including the full test matrix and the real-dotnet NuGet suites). Bugbot's latest review of b90c942 found no new issues, and its one earlier finding is fixed and resolved.

/code-review high findings not fixed

🤖 Generated with Claude Code

https://claude.ai/code/session_011f9aeS2ity8CoowhYN5FpJ

Assisted-by: Claude Code:claude-opus-5-5
Agent-mode apply, scan and vex picked the first package root holding a
package, and the default ~/.nuget/packages came before the project's
own restore folders. A project that relocates its packages folder
(globalPackagesFolder in nuget.config, RestorePackagesPath, or
repositoryPath for packages.config) had the user-wide copy patched
instead, apply reported success, and vex attested not_affected while
the build used the unpatched copy. With a cold default cache, a project
two levels below the solution root was not found at all.

Roots are now resolved the way NuGet does:
- the packages.config folder honors repositoryPath (nearest
  nuget.config wins, relative to that config's directory);
- every obj/project.assets.json under the root, at any depth,
  contributes the packageFolders its restore used, ahead of the
  global folder;
- the global folder is NUGET_PACKAGES, else the nearest
  globalPackagesFolder (user config alone in -g mode), else the
  default; a relocated folder replaces the default one.

The nuget.config reader now records the configuration/config settings.

Fixes #397
Fixes #398

Assisted-by: Claude Code:claude-opus-5-5
Covers the global-mode leg of #397 end to end: a globalPackagesFolder
in the user-level NuGet.Config must be the copy `scan -g --sync`
patches, and the stale default ~/.nuget/packages copy must stay
untouched.

Assisted-by: Claude Code:claude-opus-5-5
- Read parent configs when --cwd is relative (the default `.`):
  the config chain starts from the absolute path.
- Build the config chain once per crawl and query both settings.
- Expand %VAR% like .NET: an unset variable stays literal instead
  of dropping the setting and falling back to the default folder.
- Honor repositoryPath only when a packages.config lives under the
  root, so a shared store set in a parent or user config is not
  walked unscoped ahead of a PackageReference project's restore.
- Skip the configured stores and already-found package folders in
  the restore walk, so a big in-tree store can't use up its budget.
- Use DOTNET_CLI_HOME for the default folder off Windows, matching
  the user-config lookup.
- Make the -g CLI test write the user config where Windows reads it.

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 28, 2026 06:13
@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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Restore walk still enters package stores
    • Fixed by stripping trailing slashes from package folder paths and checking if directories are package stores when popping from the pending queue.

Create PR

Or push these changes by commenting:

@cursor push 9f55775994
Preview (9f55775994)
diff --git a/crates/socket-patch-core/src/crawlers/nuget_crawler.rs b/crates/socket-patch-core/src/crawlers/nuget_crawler.rs
--- a/crates/socket-patch-core/src/crawlers/nuget_crawler.rs
+++ b/crates/socket-patch-core/src/crawlers/nuget_crawler.rs
@@ -616,7 +616,15 @@
     let package_folders = json
         .get("packageFolders")
         .and_then(|folders| folders.as_object())
-        .map(|folders| folders.keys().map(PathBuf::from).collect())
+        .map(|folders| {
+            folders
+                .keys()
+                .map(|key| {
+                    let path = key.strip_suffix('/').unwrap_or(key);
+                    PathBuf::from(path)
+                })
+                .collect()
+        })
         .unwrap_or_default();
     let libraries = json
         .get("libraries")
@@ -684,6 +692,9 @@
     let mut pending = vec![root];
     let mut listed = 0usize;
     while let Some(dir) = pending.pop() {
+        if skip.contains(&dir) || walk.package_folders.contains(&dir) {
+            continue;
+        }
         listed += 1;
         if listed > SCOPE_DIR_BUDGET {
             walk.libraries = None;

You can send follow-ups to the cloud agent here.

The restore walk queued a directory's subdirectories before reading
that directory's own project.assets.json, so a store it names (a
solution-root RestorePackagesPath of pkgs/) was still walked. A large
store could spend the 10,000-directory budget before deeper restores
were seen, and a project-shaped folder inside it dropped the restore
scope. The walk now queues children after reading the restore, and
skips a queued directory that a later restore names.

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 b90c942. 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

Final review brief

What it does. For NuGet, agent-mode apply/scan/vex now patch the package folder the project actually restores from. Before, a warm ~/.nuget/packages won over a relocated folder, so apply exited 0 and VEX said not_affected while the build linked the unpatched copy (#397, #398). Roots are now tried in this order: the packages.config folder (repositoryPath), then the packageFolders of every obj/project.assets.json at any depth, then the global folder NuGet would use (NUGET_PACKAGES, then the nearest globalPackagesFolder, then the default).

Risk: low–medium. It only changes which NuGet copy gets picked, and the order now follows NuGet's own. The medium part: the restore walk used to run only when cwd was a project, and now runs on every local NuGet crawl. It still has the existing 10k-directory budget and skips the package stores.

Look here

Verified

Changes I made: none.

Open questions (not blocking):

  1. Store skipping and the root dedupe compare paths exactly. A relative globalPackagesFolder like ../pkgs resolves to /proj/../pkgs, which is not normalized, so an in-tree store reached through .. may still be walked. That costs walk budget, not correctness.
  2. NuGet's machine-wide configs (/etc/opt/NuGet/Config, %ProgramFiles(x86)%\NuGet\Config) aren't read. A globalPackagesFolder set only there is rare.

Auto-merge is armed, so approving sends this straight to the merge queue.


Generated by Claude Code

Merged via the queue into main with commit 846bc75 Oct 28, 2026
50 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the agent/fix-nuget-configured-package-folders branch October 28, 2026 14:08
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

4 participants