From 153825050276f96af1b638f4c2c3a4278dbf6e70 Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Wed, 4 Nov 2026 11:08:04 -0600 Subject: [PATCH 1/4] Read the governing go.work in Go discovery Go discovery read only a go.work at the project root and only members inside the root. With GOWORK unset it ignored an ancestor go.work and read the root go.mod in module mode, so in a member of a parent workspace it could attest a go.mod replace that the workspace overrides, doesn't `use`, or makes inert by version selection. Hosted and vendored modes resolve the workspace go itself would use and refuse to write such a replace. On disk, discovery now loads the governing workspace with the same go_workspace::load_with loader hosted mode uses: GOWORK (environment, then the go env -w file), else the nearest go.work at or above the root, with `use` members anywhere. Out-of-root members' requires and replaces now count toward selection and conflicts. In-root members are still re-read through the view, so recognition and parse diagnostics stay the same. A wired Socket replace that this workspace keeps from the build, at exactly the points where hosted mode's host_conflict check refuses, stays a ref (list, rollback and remove still find it) but is marked unattested with the new UnattestedKind::GoWorkspaceOverride. The same applies to a replace in the root go.work while GOWORK makes go build another one. vex_sources reports these under the existing `go_workspace_override` reason, the one vex's own go.mod gate already uses. A replace that workspace selection makes inert is still diagnosed, as in module mode. A GOWORK that go rejects still withholds every ref as patched_ref_invalid. In memory there is no directory to search from and no environment the project builds under. The root go.work and its in-root members are the workspace there, as the in-memory hosted engine reads them, and the same conflict gate applies. GOWORK keeps its old effect in memory and GOFLAGS is not read. Co-Authored-By: Claude Opus 5.5 --- crates/socket-patch-cli/CLI_CONTRACT.md | 8 +- crates/socket-patch-cli/src/commands/vex.rs | 8 +- .../src/commands/vex_sources.rs | 11 + .../src/vex/discover/golang.rs | 579 +++++++++++++++--- .../socket-patch-core/src/vex/discover/mod.rs | 6 + docs/ecosystems.md | 9 +- 6 files changed, 524 insertions(+), 97 deletions(-) diff --git a/crates/socket-patch-cli/CLI_CONTRACT.md b/crates/socket-patch-cli/CLI_CONTRACT.md index a3ed909d6..785d8cfe4 100644 --- a/crates/socket-patch-cli/CLI_CONTRACT.md +++ b/crates/socket-patch-cli/CLI_CONTRACT.md @@ -533,7 +533,7 @@ Discovery is read-only, never touches the network, and never fails the run: a ma | bun | `bun.lock`; `bun.lockb` only when there is no `bun.lock` (bun reads exactly one) | URL tuple / binary remote-tarball resolution; version from the URL leaf. A user URL or `file:` tarball tuple / record whose `-.tgz` leaf names the same `name@version` is installed from its own spec, so every ref for that `name@version` is dropped | `.socket/vendor/npm//-.tgz` tuple / local-tarball resolution | `sha512-…`, required. A 2-tuple that Bun < 1.3.10 re-saved without its digest is still a reference, but it attests only from an installed tree. | | vlt | `vlt-lock.json` (lockfileVersion absent, `0` or `1`; a BOM-prefixed, unparseable, non-object or other-version lock is not read: diagnosed, no ref). `vlt.json` (read only for its `modifiers`) and `node_modules/.vlt-lock.json` are never wiring. | a registry node (any segment) whose slot [3] is a `/patch/npm/…` URL on the patch host with the leaf `-.tgz` of its DepID's `name@version`, whose embedded `/` path (when present) is that `name@version`, and slot [1] == name; version from the DepID | a `file` node `.socket/vendor/npm//-/node_modules/` (or a user-installed `-.tgz`) with slot [1] == name; version from the path. A same-`name@version` registry node, or a diagnosed Socket-shaped one, beside it is diagnosed, no ref. | slot [2] `sha512-…`, required (a hosted node without one is no reference). A same-`name@version` node on another registry, or a diagnosed Socket-shaped one, keeps the reference but withholds the lockfile basis: only an installed tree whose every store copy verifies attests. So does a lock some vlt release discards, the conditions of `redirect_vlt_lockfile_version_missing`, `redirect_vlt_old_lockfile_ignored` and `redirect_vlt_scalar_registry_ignored` (which in-run `--vex` withholds too): every hosted reference in it keeps no pin, and one `patched_ref_unattributable` names them. | | cargo | `Cargo.lock`, `Cargo.toml`, `.cargo/config` (else `.cargo/config.toml`) | `Cargo.lock` `source = "sparse+…//index/"`, confirmed by `Cargo.toml`: a crate the root manifest declares must pin `registry = "socket-patch-"`. A reverted pin is diagnosed, no ref. Another `[[package]]` of the same `name@version` from a non-Socket source (crates.io, git), which cargo locks beside the pin for a dependent added later and builds unpatched (#679), withholds the ref (`patched_ref_unattributable`, never attested) while `rollback` / `remove` still unwind it. | `[patch.] = { path = ".socket/vendor/cargo//-" }` — primarily the root `Cargo.toml` (v5 `vendor`; key-agnostic: `` is `package` when renamed, else the key, so `-socket-` keys count), also the project config (pre-v5 wiring), live only while the lock holds a sourceless entry for it that is not in `[[patch.unused]]`; a manifest entry cargo ignores — the project config redefines its key with another path, or a `[patch."https://github.com/rust-lang/crates.io-index"]` table replaces `[patch.crates-io]` — is diagnosed (`patched_ref_invalid`), no ref | `checksum` (v1: `[metadata]`), required | -| golang | `go.mod`, `go.work`, `go.sum`, `go.work.sum` | `replace M v => patch.socket.dev/gopatch/ ` | `replace M v => ./.socket/vendor/golang//M@v` | the socket module's `/go.mod` go.sum line, required. The zip line (`h1:` of the module tree) is the locked integrity when present; `go mod tidy` prunes it when no package of the module is loaded and the build stays green, so the `/go.mod` line alone still attests from the lockfile, unless the main modules' `require`s or go.sum show the build selects another version (MVS keeps the `/go.mod` line of versions it only reads): then the replace is inert. With a `go.work` the main modules are its `use` members and the build selects by MVS over their module graph: each member's `require`s and the `go.mod` of every dependency go loads (read from its replacement directory or the module cache when the root is on disk). go keeps no member's `go.mod` tidy in workspace mode, so a member's dependency can raise the module past every `require`. A pin, zip line or not, whose replaced version a member's or a dependency's `require` raises is inert, as is one of a module that is itself a `use` member (go builds it from that directory). Sum-file zip lines are no evidence of the selection (go only appends to `go.work.sum`), and a `/go.mod`-only pin is never the lockfile basis in a workspace (`integrity_required = true`). Any mode writes the replace of a module a workspace shares into the root `go.work` (source `go.work`); a `go.work` root without a `go.mod` also reads each in-root member's `go.mod` replaces (source `/go.mod`). The sum files are the ones go reads: the root `go.sum`, or with a `go.work`, `go.work.sum` and each in-root `use` member's `go.sum` (the root's only when `.` is a member); under `GOWORK=off`, `go.work` and its replaces are ignored; a `GOWORK` naming any file other than the root's `go.work` (or a relative path, which go rejects) leaves the workspace unread: each Go ref is reported as `patched_ref_invalid` instead, as it is when `GOFLAGS` sets a `-modfile` other than the root `go.mod`. A conflicting hash or a malformed `h1:` for the socket module+version, or a line of other than three tokens anywhere in a sum file, is no pin. A replace that `require` no longer selects (`require M v'`) is inert: diagnosed, no ref. | +| golang | `go.mod`, `go.work`, `go.sum`, `go.work.sum` | `replace M v => patch.socket.dev/gopatch/ ` | `replace M v => ./.socket/vendor/golang//M@v` | the socket module's `/go.mod` go.sum line, required. The zip line (`h1:` of the module tree) is the locked integrity when present; `go mod tidy` prunes it when no package of the module is loaded and the build stays green, so the `/go.mod` line alone still attests from the lockfile, unless the main modules' `require`s or go.sum show the build selects another version (MVS keeps the `/go.mod` line of versions it only reads): then the replace is inert. With a `go.work` the main modules are its `use` members and the build selects by MVS over their module graph: each member's `require`s and the `go.mod` of every dependency go loads (read from its replacement directory or the module cache when the root is on disk). go keeps no member's `go.mod` tidy in workspace mode, so a member's dependency can raise the module past every `require`. A pin, zip line or not, whose replaced version a member's or a dependency's `require` raises is inert, as is one of a module that is itself a `use` member (go builds it from that directory). Sum-file zip lines are no evidence of the selection (go only appends to `go.work.sum`), and a `/go.mod`-only pin is never the lockfile basis in a workspace (`integrity_required = true`). Any mode writes the replace of a module a workspace shares into the root `go.work` (source `go.work`); a `go.work` root without a `go.mod` also reads each in-root member's `go.mod` replaces (source `/go.mod`). On disk the workspace is the one go resolves, read as hosted and vendored modes read it: `GOWORK`, else the nearest `go.work` at or above the project, with `use` members anywhere (in memory: the root `go.work` and its in-root members). The sum files are the ones go reads inside the project: the root `go.sum`, or with a `go.work`, `go.work.sum` (when it is the root's) and each in-root `use` member's `go.sum` (the root's only when `.` is a member); under `GOWORK=off`, `go.work` and its replaces are ignored. A Socket replace that workspace keeps from the build, exactly where hosted and vendored modes refuse to write one (its `go.work` is an ancestor's or another file `GOWORK` names, so the root `go.work` is never read; it overrides the replace; it doesn't `use` the module; another member replaces the same `module@version` elsewhere), stays a reference but is unattested (`go_workspace_override`). A `GOWORK` go rejects (a relative path, a file it cannot read or parse) reports each Go ref as `patched_ref_invalid` instead, as does a `GOFLAGS` `-modfile` other than the root `go.mod`. A conflicting hash or a malformed `h1:` for the socket module+version, or a line of other than three tokens anywhere in a sum file, is no pin. A replace that `require` no longer selects (`require M v'`) is inert: diagnosed, no ref. | | pypi | `uv.lock` (confirmed by `pyproject.toml` `[tool.uv.sources]` when present), PEP 723 `