[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.config still points the package at the hosted, grant-token-bearing patch.socket.dev/patch-registry/nuget/<token>/<uuid>/index.json feed. 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. list still reports the package as mode: hosted.
- A stray nupkg is left for the user to commit. The next
vendor --revert deletes it as vendor_orphan_removed ("vendored dir had no ledger entry").
- No
vendor_takeover_reverted_redirect event, and --dry-run shows no vendor_would_revert_redirect either.
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 a vendor_takeover_reverted_redirect advisory 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 nupkg
Matrix
| 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_of compares canonical_purl(&pin.purl) == canonical_purl(purl).
crates/socket-patch-core/src/utils/purl.rs:152: canonical_purl only strips qualifiers and percent-decodes. It doesn't fold case for case-insensitive ecosystems (NuGet; the composer case-folding in purl_eq shows the intended pattern).
crates/socket-patch-core/src/vendor/nuget_feed.rs:257: config_wired = config_text.contains(&source_key) can't tell a hosted socket-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.
[agent] Found by the scheduled NuGet / dotnet bug-hunt routine (ledger #320).
Summary
On a NuGet project that
scan --mode hostedwired, a laterscan --mode vendorednever runs the documented hosted → vendored takeover. The hosted-pin lookup compares purls case-sensitively. Lockfile discovery reports the pin aspkg:nuget/newtonsoft.json@13.0.3(lowercased id), but the crawler/API candidate ispkg:nuget/Newtonsoft.Json@13.0.3, sohosted_pin_offinds nothing.The NuGet vendored backend then sees the
socket-patch-<uuid>key that hosted mode wrote intonuget.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>/*.nupkgwith "(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..socket/vendor/state.json) is written.liststill reports the package asmode: hosted.vendor --revertdeletes it asvendor_orphan_removed("vendored dir had no ledger entry").vendor_takeover_reverted_redirectevent, and--dry-runshows novendor_would_revert_redirecteither.The
vendorcommand 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 andlistsaysvendored. So this is specific to thescan --mode vendoredpath (and likelyget --mode vendored, which shares it).Same root cause, smaller symptom: on a hosted project,
remove pkg:nuget/Newtonsoft.Json@13.0.3androllback pkg:nuget/Newtonsoft.Json@13.0.3(the purl spellingscanitself prints) failnot_found(exit 1).remove pkg:nuget/newtonsoft.json@13.0.3works (hosted_reverted). NuGet ids are case-insensitive, so either spelling should match.Expected (CLI_CONTRACT.md, "Takeover reconciliation (every hosted ecosystem, v5.0)")
--dry-runshould reportvendor_would_revert_redirect.Actual
Repro
This uses the wiremock
Backendstand-in and real nuget.org fixture restore fromcrates/socket-patch-cli/tests/e2e_nuget_dotnet_build.rs(API + hosted flat-container feed), with a realdotnet restore:Matrix
scan --mode vendoredover hostedvendorover hostedremove <Mixed.Case purl>on hostednot_found)https://patch.socket.dev/…(sed)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.