Repository navigation
NuGet scan --mode vendored over a hosted pin skips the takeover (purl case mismatch), keeps the hosted feed wired, writes no vendor ledger and still reports success #553
Description
[agent] Found by the scheduled NuGet / dotnet bug-hunt routine (ledger #320).
Summary
On a NuGet project that scan --mode hosted wired, a later scan --mode vendored never runs the documented hosted → vendored takeover. The hosted-pin lookup compares purls case-sensitively. Lockfile discovery reports the pin as pkg:nuget/newtonsoft.json@13.0.3 (lowercased id), but the crawler/API candidate is pkg:nuget/Newtonsoft.Json@13.0.3, so hosted_pin_of finds nothing.
The NuGet vendored backend then sees the socket-patch-<uuid> key that hosted mode wrote into nuget.config. Hosted and vendored use the same key, so it concludes the project is already vendor-wired. It takes the "artifact missing/stale" branch and rebuilds .socket/vendor/nuget/<uuid>/*.nupkg with "(nuget.config untouched)". The run exits 0, status: success, applied.
The result:
nuget.configstill points the package at the hosted, grant-token-bearingpatch.socket.dev/patch-registry/nuget/<token>/<uuid>/index.jsonfeed. Restores keep downloading from it, so the project still needs that network access and the token URL, which is the reason to switch to vendored.- No vendor ledger (
.socket/vendor/state.json) is written.liststill reports the package asmode: hosted. - A stray nupkg is left for the user to commit. The next
vendor --revertdeletes it asvendor_orphan_removed("vendored dir had no ledger entry"). - No
vendor_takeover_reverted_redirectevent, and--dry-runshows novendor_would_revert_redirecteither.
The vendor command on the same project works: it uses the discovered lowercase purl, so the takeover runs (eject_planned), the ledger is written, the config gets the local feed and list says vendored. So this is specific to the scan --mode vendored path (and likely get --mode vendored, which shares it).
Same root cause, smaller symptom: on a hosted project, remove pkg:nuget/Newtonsoft.Json@13.0.3 and rollback pkg:nuget/Newtonsoft.Json@13.0.3 (the purl spelling scan itself prints) fail not_found (exit 1). remove pkg:nuget/newtonsoft.json@13.0.3 works (hosted_reverted). NuGet ids are case-insensitive, so either spelling should match.
Expected (CLI_CONTRACT.md, "Takeover reconciliation (every hosted ecosystem, v5.0)")
vendoring over a hosted pin (
vendor,scan --mode vendored,get --mode vendored) first RESTORES that purl's lock entries to their default upstream registry entry … and then vendors, so the vendor ledger records the PRISTINE registry entry … The run that takes over records avendor_takeover_reverted_redirectadvisory event
--dry-run should report vendor_would_revert_redirect.
Actual
== scan --mode vendored --vendor-source service (over a hosted project)
rc=0 status success
ev applied pkg:nuget/Newtonsoft.Json@13.0.3
ev skipped vendor_prebuilt_downloaded …
ev skipped vendor_artifact_rebuilt the committed vendored nupkg for Newtonsoft.Json@13.0.3 was missing or stale; rebuilt at .socket/vendor/nuget/<uuid>/newtonsoft.json.13.0.3.nupkg (nuget.config untouched)
nuget.config: <add key="socket-patch-<uuid>" value="https://patch.socket.dev/patch-registry/nuget/<token>/<uuid>/index.json" />
.socket/vendor/: nuget/ only (no state.json)
list: [('pkg:nuget/newtonsoft.json@13.0.3', 'hosted')]
== vendor --vendor-source service (control, same starting state)
rc=0 status success
ev applied pkg:nuget/newtonsoft.json@13.0.3
.socket/vendor/: nuget/ state.json
list: [('pkg:nuget/newtonsoft.json@13.0.3', 'vendored')]
== remove pkg:nuget/Newtonsoft.Json@13.0.3 (hosted project)
rc=1 not_found: No patch found matching identifier: pkg:nuget/Newtonsoft.Json@13.0.3
== remove pkg:nuget/newtonsoft.json@13.0.3
rc=0 removed hosted_reverted
Repro
This uses the wiremock Backend stand-in and real nuget.org fixture restore from crates/socket-patch-cli/tests/e2e_nuget_dotnet_build.rs (API + hosted flat-container feed), with a real dotnet restore:
# fixture: app.csproj (net8.0, RestorePackagesWithLockFile, Newtonsoft.Json 13.0.3) + nuget.org-only nuget.config,
# restored once so packages.lock.json exists
sp() { socket-patch "$@" --json --yes --api-url $URI --org test-org --api-token fake --patch-server-url $URI; }
sp scan --mode hosted # wires socket-patch-<uuid> → hosted feed, re-pins lock
# (optional) sed the source URL to https://patch.socket.dev/... — same result
sp scan --mode vendored --vendor-source service # exit 0, success, no takeover
grep socket-patch nuget.config # still the hosted URL
ls .socket/vendor # nuget/ only, no state.json
sp list # mode: hosted
NUGET_PACKAGES=$(mktemp -d) dotnet restore --locked-mode # NU1803 (http stand-in): downloads from the hosted feed
sp vendor --revert # vendor_orphan_removed, deletes the nupkgMatrix
| OS | SDK | config URL | autocrlf | scan --mode vendored over hosted |
vendor over hosted |
remove <Mixed.Case purl> on hosted |
|---|---|---|---|---|---|---|
| Linux | 8.0.131 | http stand-in | false | fail (4 runs) | pass | fail (not_found) |
| Linux | 8.0.131 | http stand-in | true | fail | untested | untested |
| Linux | 8.0.131 | https://patch.socket.dev/… (sed) |
false | fail | untested | untested |
The bug is in OS-independent purl matching, so macOS and Windows weren't probed.
First bad: current main 61cfb9b (the v5 lockfile-discovered takeover from #280). v4.0.0 used the redirect ledger, so I didn't bisect further.
Suspect code
crates/socket-patch-cli/src/commands/vendor.rs:2168-2172:hosted_pin_ofcomparescanonical_purl(&pin.purl) == canonical_purl(purl).crates/socket-patch-core/src/utils/purl.rs:152:canonical_purlonly strips qualifiers and percent-decodes. It doesn't fold case for case-insensitive ecosystems (NuGet; the composer case-folding inpurl_eqshows the intended pattern).crates/socket-patch-core/src/vendor/nuget_feed.rs:257:config_wired = config_text.contains(&source_key)can't tell a hostedsocket-patch-<uuid>source from a vendored one (it doesn't check the value is.socket/vendor/nuget/<uuid>). That turns the missed takeover into a silent false success instead of a refusal.crates/socket-patch-cli/src/commands/remove.rs:40(hosted_pins_matching) and rollback's purl targeting: the same case-sensitive comparison.
Activity
nuget: 2026-10-02 11:44 run (#553)
mikolalysenko commentedon Oct 2, 2026
[agent] Triaged as
priority:p3(NuGet). It isn't a duplicate, and I found no open or merged PR that fixes it. I confirmed the cause on main61cfb9b:hosted_pin_of(crates/socket-patch-cli/src/commands/vendor.rs~L2168) comparescanonical_purl(..)strings.canonical_purl(crates/socket-patch-core/src/utils/purl.rs:152) only strips qualifiers and percent-decodes. It never folds case, so it misses the case-insensitive NuGet id (Newtonsoft.Jsonvsnewtonsoft.json).remove/rollbackpurl targeting has the same mismatch.The
nuget_feed.rsconfig_wiredkey-only check is what turns the missed takeover into a silent success. That check should also be tightened as part of the fix.Generated by Claude Code
mikolalysenko commentedon Oct 2, 2026
[agent] New information from the NuGet / dotnet bug-hunt routine (ledger #320).
get --mode vendoredover a hosted project has the same defect. This is on main6cd3754(Linux, SDK 8.0.131), and I reproduced it twice: once with the patch UUID as the identifier and once withpkg:nuget/Newtonsoft.Json@13.0.3.After
scan --mode hosted, runningget <id> --mode vendored --json --yesexits 0 withstatus: success. Its embedded vendor envelope reportsappliedandvendor_prebuilt_downloaded, thenvendor_artifact_rebuiltwith "… rebuilt at .socket/vendor/nuget//newtonsoft.json.13.0.3.nupkg (nuget.config untouched)". Afterwards:http://…/patch-registry/nuget/…/index.jsonsource is still the one wired innuget.config..socket/vendor/state.json.list --jsonreports nothing.So
gettakes the same "artifact rebuilt" branch asscan --mode vendored: the vendored backend reads hosted'ssocket-patch-<uuid>key as its own wiring and skips the takeover.Generated by Claude Code
mikolalysenko commentedon Oct 2, 2026
[agent] New information from the NuGet bug-hunt (ledger #320), main
045d7ec, Linux SDK 8.0.131: on a vendored project the purl case sensitivity runs the opposite way to hosted.remove pkg:nuget/Newtonsoft.Json@13.0.3/rollback pkg:nuget/Newtonsoft.Json@13.0.3(the spellinglistandscanprint):vendor_reverted/vendoredReverted, exit 0.remove pkg:nuget/newtonsoft.json@13.0.3/rollback pkg:nuget/newtonsoft.json@13.0.3:not_found"No patch found matching identifier", exit 1, andnuget.configstays wired.Hosted only accepts the lowercase spelling (see above), and vendored only accepts the mixed-case one. NuGet ids are case-insensitive, so a fix should match the purl name case-insensitively in
remove/rollbackfor both backends, not just make hosted record the other spelling.Generated by Claude Code