Stop hosted Go redirects claiming unpatched deps - #252
Conversation
Hosted mode counted a Go module as redirected whenever any project file held the patch-server origin (an already hosted package-lock.json) or leftover gopatch go.sum lines, even when the go.mod rewrite was refused, so VEX attested modules that still built unpatched. Go deps now count only when the rewriter reports the replace and both go.sum lines in place. A module that go.mod does not require and go.sum does not list (another project's module in the shared module cache) is refused instead of getting an inert replace. Switching a vendored Go module to hosted mode now reverts its vendored copy and ledger entry first, as cargo and npm already did, so the next vendor run no longer switches it back. Replaces the CLI did not write are left alone: only ./.socket/... paths and exactly patch.socket.dev/gopatch/<uuid> are socket-owned, not a sibling checkout's .socket/vendor copy or a deeper gopatch path. Duplicate socket replaces for one module collapse to one, a refreshed directive keeps its trailing comment, quoted module paths are recognized, and %2B-encoded +incompatible versions resolve in vendor and apply. Assisted-by: Claude Code:claude-opus-5-5
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Reconcile drops decoded Go copies
- Changed reconcile_go_redirects to use canonical_purl instead of strip_purl_qualifiers, which now properly decodes percent-encoded characters like %2B to + for matching manifest keys against disk-based PURLs.
You can send follow-ups to the cloud agent here.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit ad8d067. Configure here.
…mparison The reconcile_go_redirects function was using strip_purl_qualifiers to build desired_bases, which doesn't decode percent-encoded characters. However, parse_golang_purl now decodes characters like %2B to +, so on-disk copies use the decoded version (+incompatible) while manifest keys still have the encoded form (%2Bincompatible). This mismatch caused reconciliation to treat valid copies as orphans and remove them along with their go.mod replace directives. The fix uses canonical_purl instead, which both strips qualifiers and decodes, ensuring proper matching between manifest keys and disk-based PURLs. Added test_reconcile_keeps_percent_encoded_version to prevent regression.
The previous commit stopped apply reconcile from deleting a freshly applied +incompatible go-patches copy (manifest keys spell it %2Bincompatible, the copy directory uses the decoded version). Record it under Unreleased > Fixed. Assisted-by: Claude Code:claude-opus-5-5
Rolling back a hosted Go redirect appended the pruned upstream go.sum lines to the end of the file, so go.sum did not come back byte for byte and stayed out of go's order until the next go mod tidy. The lines now go back where go sorts them: module path, then semver version, then the /go.mod suffix. CRLF projects also unwind cleanly. The socket module's go.sum lines are removed line by line instead of as an LF-joined fragment, and removing the replace line takes its CRLF with it, so no socket lines or blank lines are left behind. Assisted-by: Claude Code:claude-opus-5-5
Vendoring a hosted-redirected Go module rewrote its replace in place but kept the redirect-ledger record, the socket module's go.sum lines, and the pruned upstream go.sum lines. The ledger kept claiming a hosted redirect that was gone, and vendor --revert left go.sum without the upstream lines. Go now gets the same per-purl takeover revert as cargo and npm. The module's replace and socket go.sum lines are removed, the upstream lines come back in go's order, and the record is dropped before the vendored replace is written. A replace that no longer matches the recorded one refuses the takeover and leaves the files alone. Scoped rollback and remove of one hosted Go module now work as well. Assisted-by: Claude Code:claude-opus-5-5
When a vendored Go module was missing from the module cache, the pristine fetch skipped off and direct entries in GOPROXY and fell back to proxy.golang.org. That sent module paths to the public proxy even when go itself would never ask one, including for GOPRIVATE modules. The fetch now follows go: if the first GOPROXY element is off or direct, or the module matches GONOPROXY (default GOPRIVATE), it is refused with the usual unverifiable warning and not-installed skip. An explicit SOCKET_GOPROXY still wins. Assisted-by: Claude Code:claude-opus-5-5
|
[agent] I added four signed commits on top of Bugbot's autofix 943bbfd, which fixed the
Each fix started with a failing test. Full workspace tests: 8029 passed, and the only failures are the known environmental gem/bundler ones. clippy |
Resolves the CHANGELOG/CLI_CONTRACT and go_sum_edit.rs conflicts by keeping both sides. The hosted ownership check now accepts the same patch.socket.dev/gopatch/<uuid>[/vN] shapes VEX discovery does, and VEX discovery still flags Socket-looking replace targets the CLI does not own (another checkout's vendored copy, an absolute path, a malformed hosted module) instead of skipping them silently. Assisted-by: Claude Code:claude-opus-5-5
|
[agent] Merged main (#251, v5 manifest-less VEX) into this branch as signed merge 4be201a (no force-push). Conflicts in CHANGELOG/CLI_CONTRACT and go_sum_edit.rs were resolved by keeping both sides, plus two compile fixes after the merge ( |


LLM Description written by Claude Code:claude-opus-5-5
Summary
An audit of the Go paths turned up seven suspected bugs. Each one below was first reproduced by a new test that failed on
main; all seven reproduced and are fixed here. Follow-up commits fix four more Go-path bugs that a compatibility capture run found (B9–B12 below), plus Bugbot's reconcile regression from the B4 fix. The worst one: hosted mode could record a Go module in the redirect ledger, sovexattested the patch, even though the go.mod rewrite had been refused and the build still linked the unpatched module.B1: hosted confirmation matched the goproxy
indexUrlpackage-lock.jsonis already hosted-redirected tohttps://patch.socket.dev/..., plus a Go dep whose rewrite is refused (for example, a userreplace M V => ../my-fork). A related input: stalepatch.socket.dev/gopatch/<uuid>go.sum lines left behind after the user removed the replace, with the graph now at another version.redirected: 1, a ledger record, and a VEX attestation for an unpatched module. The confirmation probe usedtext.contains(indexUrl), and for goproxy that is the bare originhttps://patch.socket.dev. It also matched on the module path in any file, go.sum included.confirmed_golang_uuids, which requires the go.mod replace plus both go.sum lines, written by this run or already in place. Hosted confirmation forpkg:golang/uses that set alone, like cargo. The substring probe no longer looks at Go module paths.B2: hosted takeover of a vendored Go module left the vendored state behind
vendorM, thenget/scan --mode hostedfor M..socket/vendor/state.jsonstill claimed M and.socket/vendor/golang/<uuid>/stayed on disk. The nextvendorrun switched the module back to vendored.pkg:golang/joins cargo and npm as takeover-capable. The existing per-purlrevert_go_vendorruns first (wiring, copy and ledger entry), then the redirect.B3: hosted wrote a replace for a module outside the build graph
require, and go.sum has no line for it at that version.redirect_golang_not_in_module_graph, and nothing is written. Three things still count as membership: arequireentry, go.sum lines at that version (a go 1.16 transitive dep), or our own replace already pinned at that version (a re-run after the prune).B4:
parse_golang_purldid not percent-decodepkg:golang/github.com/foo/bar@v2.0.0%2Bincompatible.v2.0.0%2Bincompatible. The crawler looked forbar@v2.0.0%2!bincompatibleand never found the module. If it had, the replace and the copy directory would have carried%2B.Cow, and call sites shadow to&str.B6: ownership detection used substring and prefix matching
replace M V => ../sibling/.socket/vendor/golang/<uuid>/M@V, or a replace ontopatch.socket.dev/gopatch/<uuid>/extra../.socket/vendor/golang/or./.socket/go-patches/at the project root. A hosted module is socket-owned only if it is exactlypatch.socket.dev/gopatch/<canonical uuid>(the newis_hosted_module_path). The hosted rewriter now refuses any other module path withredirect_golang_untrusted_module_path.B7: only the first socket-owned directive was refreshed
replace M Vline.B8: an in-place rewrite dropped trailing comments, and quoted paths were not unquoted
replace M V => ./.socket/go-patches/M@V // CVE-…. A second input:require "github.com/x/y" v1.4.2andreplace "github.com/x/y" v1.4.2 => ../fork.// …is kept on refresh. The replace and require parsers now unquote"…"and`…`tokens.B9: hosted rollback put the pruned go.sum pair back in the wrong place
get --mode hostedfor M, thenrollback, on a go.sum where M's lines are not last./go.mod, then hash). The socket module's go.sum lines are removed line by line, the duplicate-line ambiguity guard still applies, and removing a fragment takes its\r\nwith it.B10: vendoring over a hosted Go module left the hosted state behind (B2 in reverse)
get --mode hostedfor M, thenvendororget --mode vendored.redirect-state.jsonkept M's record, go.sum kept thepatch.socket.dev/gopatch/<uuid>lines, and the upstream pair stayed pruned, sovendor --revertleft a go.sum missing M.pkg:golang/joins cargo and npm inredirect_revert_supported. The newrevert_golang_redirect_purlclaims the purl's replace, go.sum and prune edits, unwinds them through the golang replay inverses (staged all-or-nothing), and drops the record. A go.mod replace for M that no longer matches the recorded one refuses asredirect_revert_failed, leaving the files untouched. This also makes scopedrollback <purl>/remove <purl>work for a single hosted Go module.B11: vendored fetch ignored
GOPROXY=off/directandGOPRIVATEGOPROXY=offordirect, or a module that matchesGOPRIVATE/GONOPROXY.goproxy_base()skippedoff/directand fell back tohttps://proxy.golang.org, which sent the module path to the public proxy.GONOPROXY(defaultGOPRIVATE, usingMatchPrefixPatternssemantics) means no proxy. Those cases are refused before any network I/O (vendor_fetch_unverifiable, thenpackage_not_installed). An explicitSOCKET_GOPROXYstill wins.B12: a failed vendored
getkept the patch in.socket/manifest.jsondetached: true, no manifest written). No change here.Bugbot: reconcile pruned decoded
+incompatiblecopiesreconcile_go_redirectsnow compares bycanonical_purl. The follow-up commit only adds its CHANGELOG entry.Tests
crates/socket-patch-cli/tests/e2e_golang_hosted_state.rs(real binary + wiremock, no go toolchain needed):refused_go_rewrite_is_not_confirmed_by_another_lockfile(B1),leftover_go_sum_lines_do_not_confirm_a_refused_rewrite(B1/B3),hosted_takeover_of_vendored_module_removes_vendored_state(B2).golang_module_outside_the_graph_is_refused(B3)golang_confirms_only_landed_redirects(B1)golang_module_path_with_extra_segments_refused(B6)test_parse_golang_purl_percent_decodesandtest_find_by_purls_percent_encoded_version(B4)test_detect_owner_ignores_foreign_socket_dirsandtest_hosted_owner_requires_exact_uuid_module(B6)test_upsert_collapses_duplicate_socket_directives(B7)test_upsert_refresh_keeps_trailing_commentandtest_quoted_module_paths_are_unquoted(B8)e2e_golang_hosted_state.rs:hosted_rollback_restores_go_sum_byte_for_byte(B9) andvendored_takeover_of_hosted_module_unwinds_the_redirect(B10)replay::reinsert_restores_the_go_sorted_position(B9, LF and CRLF)takeover::golang_per_purl_revert_unwinds_only_that_module(B10, LF and CRLF, two modules) andgolang_per_purl_revert_refuses_a_drifted_replaceregistry_fetch::golang_fetch_never_uses_a_proxy_go_would_not(B11). The two oldergoproxy_base_*tests are re-pinned: anoff/directfirst element now refuses instead of falling back.mainand pass here. Existing fixtures that usedgopatch/some-uuidnow use a canonical uuid, anddetect_owner("sub/.socket/go-patches/…")is nowNone.cargo test --workspace --all-features --no-fail-fast(with the follow-ups): 8029 passed. The only failures are the 12 tests insetup_matrix_gem.rs, and they are environmental: this host's bundler is 1.17.2, which setup correctly refuses. The real-go capstones (e2e_golang_hosted_build,e2e_vendor_golang_build,e2e_golang_build,e2e_golang_redirect) ran against go 1.26.3 and pass.cargo clippy --workspace --all-features -- -D warnings(with and without--tests): clean.+incompatibleshape, 12 native go toolchains (1.16–1.27) × 4 vendored modes: every build, replace,go list, run, tidy, repeat and rollback check passes. The only failing check is the depscan harness'smanifestPresenceexpectation forvendored-get/vendored-service(24 of 48). That expectation predates Cleanup: no .socket residue, locks that never outlive a command, manifest-free vendored mode #247's manifest-free vendored mode and is a harness update, not a CLI bug.cargo fmt --checkis not clean onmainitself (about 60 pre-existing files). The hunks in this PR are rustfmt-clean.CHANGELOG
[Unreleased] › Fixedand the CLI_CONTRACT hosted confirmation rules are updated. The follow-ups also update CLI_CONTRACT's takeover reconciliation, per-purl revert list andSOCKET_GOPROXYrows.redirect_golang_not_in_module_graphis a new additive code.🤖 Generated with Claude Code
Note
Medium Risk
Changes hosted-mode ledger/VEX semantics and go.mod rewrite rules for Go; incorrect confirmation was a security/attestation bug, but behavior shifts may surprise repos that relied on false positives or loose ownership matching.
Overview
Hosted Go redirects now only count a module as redirected when the rewriter reports it in
confirmed_golang_uuids(go.modreplaceplus both socket-module go.sum lines)—not when unrelated files mention the patch-server origin or stale go.sum lines. Refused rewrites no longer land in the redirect ledger or get VEX attestation.Vendored → hosted for Go matches cargo/npm: the hosted run reverts vendored wiring, the committed copy, and the vendor ledger entry before applying the hosted redirect.
Graph and ownership guards: modules absent from
requireand go.sum at the patched version are refused withredirect_golang_not_in_module_graph. Socket-ownedreplacetargets are limited to./.socket/go-patches/…,./.socket/vendor/golang/…, and exactlypatch.socket.dev/gopatch/<canonical uuid>; foreign or deeper paths are left alone or refused.PURL and go.mod editing: golang PURLs percent-decode versions (e.g.
%2Bincompatible). The go.mod editor collapses duplicate socket-owned replaces, preserves trailing//comments, and unquotes quoted module paths inrequire/replace.CHANGELOG, CLI contract, core unit tests, and new CLI e2e tests (
e2e_golang_hosted_state.rs) document and lock in the behavior.Reviewed by Cursor Bugbot for commit ad8d067. Configure here.