Skip to content

feat(hosted): golang hosted redirect via fork-replace + committed go.sum pin (free tier) - #173

Open
Mikola Lysenko (mikolalysenko) wants to merge 5 commits into
mainfrom
feat/golang-hosted-redirect
Open

feat(hosted): golang hosted redirect via fork-replace + committed go.sum pin (free tier)#173
Mikola Lysenko (mikolalysenko) wants to merge 5 commits into
mainfrom
feat/golang-hosted-redirect

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

What

Makes scan --mode hosted work for Go (free tier), overturning docs/design/golang-hosted-no-go.md. The committable shape:

go.mod:  replace github.com/foo/bar v1.4.2 => patch.socket.dev/gopatch/<uuid> v1.4.2-socketpatch.1
go.sum:  patch.socket.dev/gopatch/<uuid> v1.4.2-socketpatch.1 h1:…          (flavor zip dirhash)
         patch.socket.dev/gopatch/<uuid> v1.4.2-socketpatch.1/go.mod h1:…   (served .mod bytes)

The load-bearing fact (validated empirically against go 1.26, with a bogus-GOSUMDB tripwire): go consults the checksum database only for modules absent from go.sum — committed hashes are the committable per-module sumdb exemption the no-go doc said Go lacked. A grant-free, content-addressed module path keeps the build-once converter (one artifact per patch), and free-tier publicity makes proxy.golang.org caching a robustness win instead of a leak. Paid tier stays vendored, as documented (docs/design/golang-hosted.md carries the full validated-claims list and the server requirements).

Changes

  • rewrite_golang is now a real rewriter (was: unconditional redirect_golang_unsupported). Activates per-dep on registry_override.kind == "goproxy"; fails closed (per-dep redirect_golang_* warnings, zero partial writes) on missing/malformed hashes (h1: + 44-char base64), a module path outside patch.socket.dev/gopatch/, whitespace/control chars in any token (go.mod/go.sum injection guard), a require-version mismatch (a version-pinned replace the graph doesn't select is SILENTLY inert — validated), or a user-authored replace conflict. References without the override keep the historical warning, so behavior is unchanged until the server publishes hosted Go modules (production has zero golang patches today; server half: SocketDev/depscan#24218).
  • go_mod_edit: ReplaceOwner::Hosted (ownership = RHS module path under the socket namespace), module-target replace parsing/upsert, in-place cross-owner takeover, gofmt-shaped appends (tidy-stable). New pure go_sum_edit: sorted upsert of the flavor's two lines + prune of the replaced original's lines (removed lines ride the ledger original).
  • Cross-mode safety (adversarially reviewed, 9 confirmed findings fixed): local apply refuses a Hosted-owned replace (taking it over would strand the pruned go.sum lines and brick later builds); apply --check exempts hosted-owned modules from MissingReplace drift; vendor takeover of a hosted replace is ledger-recorded and revert warns to re-run scan --mode hosted or go mod tidy; a committed-but-inert stale pin is reconciled away so it can't false-confirm ledger/VEX.
  • scan --mode hosted wiring: go.mod/go.sum join the candidate files, the redirected-confirmation matcher accepts the socket module path (a Go rewrite contains no URL), and the server's identifiers-borne hash pair (goZipDirhashH1/goModH1) merges into the normalized integrity — the tarball's dirhashH1 stays the original-path flavor that released vendor-mode CLIs verify against.

Testing

  • Capstone e2e_golang_hosted_build.rs: a fresh "day-2 machine" (empty caches, default -mod=readonly, bogus GOSUMDB that fails loudly if ever consulted, hostile ambient env baked in as RED guards) builds the PATCHED module from the committed go.mod+go.sum alone; go mod tidy is a byte-level no-op; a tampered hash dies with go's checksum SECURITY ERROR; a positive tripwire control proves the bogus sumdb actually fires when a line is missing.
  • Golden fixture golang/gomod/basic pins the byte contract the depscan TS twin matches (its inline copy already passes there); ~30 unit tests across the editors/rewriter; full suites green.
  • Merge order: this PR and depscan#24218 are independent to merge, but the depscan submodule bump that follows this merge must land AFTER depscan#24218 (its golden harness has no skip list).

🤖 Generated with Claude Code


Note

Medium Risk
Changes committed go.mod/go.sum and cross-mode Go replace ownership; mistakes could brick -mod=readonly builds or strand go.sum state, though fail-closed checks and extensive tests mitigate this. Production impact is gated on server publication of gopatch artifacts.

Overview
Adds free-tier Go hosted mode for scan --mode hosted when a patch reference includes a goproxy registry override. The rewriter commits a fork-style replace onto patch.socket.dev/gopatch/<uuid> plus the socket module’s two go.sum h1: lines, prunes the replaced original’s lines for tidy-stable output, and fails closed (per-dep redirect_golang_* warnings, no partial writes) on bad hashes, untrusted module paths, require/version mismatch, or user replace conflicts. References without the override still emit redirect_golang_unsupported.

Implementation: real rewrite_golang (replaces the unconditional no-go stub), new go_sum_edit, ReplaceOwner::Hosted and hosted upsert in go_mod_edit, wire fields goModH1 / goModuleVersion with gopatch hash merge in scan/hosted.rs, and cross-mode rules (local apply refuses hosted-owned replaces; vendor takeover/revert warnings; stale pin reconciliation).

Docs & tests: golang-hosted.md supersedes the no-go doc for free tier; ecosystems matrix and production e2e updated. Golden fixture golang/gomod/basic, large unit suite, and e2e_golang_hosted_build.rs (bogus GOSUMDB, tidy no-op, tamper SECURITY ERROR). No behavior change in production until the server publishes grant-free gopatch modules.

Reviewed by Cursor Bugbot for commit 0b18bd4. Configure here.

…sum pin (free tier)

Overturns docs/design/golang-hosted-no-go.md for the free tier. The
committable shape: a fork-style `replace <mod> <ver> =>
patch.socket.dev/gopatch/<uuid> <ver>-socketpatch.<n>` in go.mod plus the
socket module's two h1: lines in go.sum (and the replaced original's lines
pruned — the tidy-stable state). Day-2 machines need zero configuration:
go consults the checksum database only for modules absent from go.sum, so
the committed pair is the entire redirect — validated empirically per
docs/design/golang-hosted.md and pinned by e2e_golang_hosted_build.rs
(fresh caches, bogus-GOSUMDB tripwire, tidy byte-level no-op,
tampered-hash SECURITY ERROR).

- redirect/mod.rs: real rewrite_golang, gated per-dep on
  registry_override.kind == "goproxy"; fails closed (redirect_golang_*
  warnings, no partial writes) on missing hashes, out-of-namespace module
  path, require-version mismatch, or user replace conflict; references
  without the override keep redirect_golang_unsupported (paid tier).
  Schema: Integrity.goModH1 + identifiers.goModuleVersion (additive).
- go_mod_edit.rs: ReplaceOwner::Hosted (RHS-prefix ownership under
  patch.socket.dev/gopatch/), module-target parse + hosted upsert with
  cross-owner in-place takeover; appended directives now gofmt-shaped
  (blank line before the stanza) so tidy stays a no-op.
- go_sum_edit.rs (new): pure sorted upsert / exact-version prune /
  prefix removal; pruned lines ride the ledger `original` for revert.
- scan/hosted.rs: go.mod + go.sum join REDIRECT_CANDIDATE_FILES; the
  redirected-confirmation matcher accepts the socket module path (a Go
  rewrite contains no artifact/index URL).
- Golden fixture golang/gomod/basic pins the byte contract the depscan
  TS twin must match; prod e2e golang leg becomes a both-worlds shape
  guard.
- docs: design doc (incl. depscan server requirements: gopatch artifact
  flavor + token-free GOPROXY routes + go-get meta + write-once
  invariant), ecosystems matrix, README, CLI contract, changelog.

Server-side publication is the remaining half: production publishes no
golang hosted modules yet, so CLI behavior is unchanged until depscan
ships the goproxy override.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rust per adversarial review

Nine confirmed findings from the post-implementation review, all fixed:

- Local apply now FAILS CLOSED on a Hosted-owned replace instead of
  silently taking it over (which would strand pruned go.sum lines and
  break every -mod=readonly build once the directive is later dropped);
  the error names the way out. Hosted->local stays one-way by design —
  hosted-takes-over-local reconciles go.sum in the same pass.
- apply --check no longer reports MissingReplace drift for a module a
  hosted redirect legitimately took over (exemption now matches Vendor
  AND Hosted owners).
- vendor takeover of a hosted replace is visible: the wiring record
  captures the hosted directive text as `original` (action Rewritten),
  a vendor_takeover warning explains the go.sum implications up front,
  and vendor --revert detects the hosted prior from the namespace
  prefix in `original` and names the recovery (`scan --mode hosted`
  re-run or `go mod tidy`).
- Injection guard: every server-controlled token written into the
  line-oriented go.mod/go.sum (module paths, versions) is refused on
  whitespace/control characters (redirect_golang_unsafe_coords), and
  h1 hashes must be exactly `h1:` + 44-char base64.
- A committed-but-inert socket pin (require bumped past the patch
  version) is now reconciled away — directive and its go.sum lines
  removed with `removed` edits — so the stale module path can no
  longer confirm the dep as redirected (ledger + VEX) while go links
  the unpatched version.
- Hosted takeover of an existing socket directive records the replaced
  text in the ledger `original` with action `updated` (was: added,
  original None — the pre-redirect state was lost to revert).
- e2e hardening: GOENV=off (empty env-var pins don't defeat
  `go env -w` config), a chmod-on-drop guard so read-only GOMODCACHE
  trees don't leak from the tempdir on assertion failure, and a
  positive tripwire control proving the bogus GOSUMDB actually fires
  when consulted (a missing go.sum line must fail with the invalid
  verifier error).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…to integrity

The server carries the hosted-Go hash pair (goZipDirhashH1 + goModH1) on
the goproxy override's IDENTIFIERS, not the tarball artifact's integrity
— the tarball dirhashH1 must stay the original-path flavor that released
CLIs verify vendor-mode downloads against (the h1 dirhash covers entry
names, so the two flavors hash differently despite identical contents).

scan --mode hosted now merges the pair into the normalized
DepOverride.integrity (both-or-neither: a half pair still trips the
rewriter's fail-closed integrity check), mirroring the yarnBerry10c0
merge. Schema: identifiers gain goZipDirhashH1/goModH1 (additive).
Design doc's server-requirements section updated to the final contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The tidy-stable append writes ONE blank stanza separator before its
single-line directive; removal left that blank behind, so a
vendor/apply -> revert round-trip no longer restored go.mod
byte-identical (caught by e2e_vendor_golang_build in CI — a suite the
local sweep after the append change missed). Removal now drops the
preceding blank when it is not separating the directive from real user
content below (next line EOF or blank); a blank guarding user content
survives. Byte-identical round-trip pinned for both the path-target
and hosted module-target flavors, plus the user-content-below case.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant