From e4ee6952a46aaa35eb214f2713adb9e17f7e91e9 Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Thu, 24 Sep 2026 13:54:20 -0400 Subject: [PATCH 01/13] feat(vendor): wire cargo patches through Cargo.toml [patch] Vendored cargo patches are now wired through a Socket-owned [patch.crates-io] key in the workspace-root Cargo.toml instead of .cargo/config, so the patch uuid is visible from the manifest alone. Legacy config wiring is migrated on re-run, repair and uuid bumps. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/ci.yml | 33 + CHANGELOG.md | 46 +- README.md | 2 +- crates/socket-patch-cli/CLI_CONTRACT.md | 33 +- .../src/commands/repair_vendor.rs | 49 + .../src/commands/scan/hosted.rs | 29 +- .../socket-patch-cli/src/commands/vendor.rs | 3 +- .../tests/covgap_commands_scan_hosted.rs | 121 +- .../tests/e2e_vendor_cargo_build.rs | 971 +++++++- .../tests/e2e_vex_lockfile/cargo.rs | 158 +- .../socket-patch-cli/tests/e2e_vex_vendor.rs | 38 +- .../tests/mode_migration_cargo.rs | 26 +- crates/socket-patch-core/src/vendor/cargo.rs | 2218 ++++++++++++++--- .../src/vendor/cargo_config.rs | 302 ++- .../src/vendor/cargo_manifest.rs | 1220 +++++++++ .../src/vendor/lock_inventory/mod.rs | 5 +- crates/socket-patch-core/src/vendor/mod.rs | 3 +- crates/socket-patch-core/src/vendor/state.rs | 63 + .../src/vex/discover/cargo.rs | 254 +- docs/ecosystems.md | 65 +- 20 files changed, 5064 insertions(+), 575 deletions(-) create mode 100644 crates/socket-patch-core/src/vendor/cargo_manifest.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d6155e7..8d2396f2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1458,6 +1458,39 @@ jobs: cargo test -p socket-patch-cli --test e2e_redirect_cargo_build --test e2e_vendor_cargo_build --test mode_migration_cargo cargo test -p socket-patch-cli --test e2e_safety_cargo_build -- --ignored + # Manifest `[patch]` (the v5 vendored cargo wiring) on toolchains older + # than the 1.56 floor of config-file `[patch]`. The old-toolchain tests + # skip when none is installed; this leg installs two and requires them. + cargo-old-toolchains: + name: cargo old toolchains (manifest [patch]) + needs: test + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Install Rust + run: rustup show + - name: Install the old cargos under test + run: | + rustup toolchain install 1.41.0 --profile minimal + rustup toolchain install 1.56.0 --profile minimal + - name: Cache cargo + uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + with: + key: cargo-old-toolchains + save-if: ${{ github.ref == 'refs/heads/main' }} + - name: Vendored manifest [patch] on old cargo + shell: bash + env: + SOCKET_PATCH_CARGO_E2E_REQUIRED: '1' + SOCKET_PATCH_CARGO_OLD_TOOLCHAINS_REQUIRED: '1' + run: | + set -euo pipefail + cargo test -p socket-patch-cli --test e2e_vendor_cargo_build -- old_toolchain --nocapture + # ---------------------------------------------------------------------- # Experimental `setup`-flow matrix (NON-BLOCKING). # diff --git a/CHANGELOG.md b/CHANGELOG.md index 8540e431..b6c72a76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,13 +19,57 @@ into the new version's section — see docs/releasing.md. > **Semver note:** this entry changes `rollback`'s default behavior, narrows > the meaning of its existing `vendored: []` JSON key, makes vendored mode -> manifest-free, turns a plain non-TTY `scan` report-only, and makes `vex` +> manifest-free, moves vendored cargo wiring from `.cargo/config*` into +> `Cargo.toml`, turns a plain non-TTY `scan` report-only, and makes `vex` > refuse to attest stale ledger records and corrupt vendor ledgers — all > MAJOR per CLI_CONTRACT.md's semver policy — so it ships as the next major > release (v5.0). ### Changed (BREAKING) +- **Vendored cargo wiring moved to `Cargo.toml`.** `vendor` / `scan` / + `get --mode vendored` write the `[patch.crates-io]` path entry into the + workspace-root `Cargo.toml` (beside the `Cargo.lock` it detaches) instead + of `.cargo/config.toml` / `.cargo/config`, so Socket scanners can recover + the patch uuid from the manifest alone and single-version wiring builds + on cargo older than 1.56 (the floor of config-file `[patch]`); two + vendored versions of one crate additionally need `--offline` (or a + reachable registry index) on such old cargo. The edit is + format-preserving (comments, ordering, CRLF / mixed line endings, a + UTF-8 BOM and the trailing-newline state survive; a revert restores the + manifest byte for byte and keeps a user's own `[patch]` / + `[patch.crates-io]` headers). The key is always the Socket-owned + `-socket-` with `package = ""`, never the bare crate + name: cargo lets a config-file `[patch]` item (project, ancestor + directory or `$CARGO_HOME`) replace the manifest item with the same key + whatever its version, so a crate-named key could be silently shadowed — + and two vendored versions of one crate get distinct keys instead of + clobbering each other (the config wiring keyed by crate name let the + second overwrite the first). The ledger's `cargo_patch_entry` record now + names `Cargo.toml` (its `key` is the TOML key). New refusals, each before + any write: `cargo_manifest_unreadable`, `cargo_manifest_unparseable`, + `cargo_manifest_symlink_unsupported` (vendor and revert), + `cargo_manifest_not_workspace_root` (run from a workspace member, whose + `[patch]` cargo ignores) and `cargo_manifest_patch_source_alias` (the + manifest spells crates.io by URL in `[patch."https://github.com/rust-lang/crates.io-index"]`, + which replaces `[patch.crates-io]` wholesale); + `user_authored_patch_entry` now covers user entries in `Cargo.toml` and + in every cargo config file cargo merges (project, ancestors, + `$CARGO_HOME`) and matches by crate (`package` or key), sparing a path + patch that is provably another version. **Old wiring migrates + automatically**: a re-run or `repair` moves a Socket-owned + `.cargo/config*` entry into `Cargo.toml` (`cargo_wiring_migrated` note; a + migrating vendor re-run reports the package `applied`) and cleans a + config file / `.cargo/` the move emptied — a legacy entry that cannot be + removed fails the run and unwinds it (`cargo_legacy_wiring_kept`) — while + `rollback` / `remove` / `vendor --revert` / GC / hosted takeover remove + both spellings. Projects hit by the pre-v5 multi-version overwrite (a + detached lock entry nothing wired) are healed by a re-run or `repair` + (`cargo_wiring_restored`). User config entries are never touched. VEX + discovery reads the manifest first (key-agnostic), skips a manifest entry + that cargo ignores (a same-key project-config item or a URL-spelled + crates.io table replaces it), and still honors pre-v5 config wiring. The + hosted takeover's missing-ledger guard is now per version. - **Binary Bun lockfiles are patched natively in place.** Hosted and vendored modes read and rewrite `bun.lockb` formats 1–3 directly, including mode changes, repair, and scoped rollback. Binary-to-text conversion, migration diff --git a/README.md b/README.md index 063359d0..ea4878b5 100644 --- a/README.md +++ b/README.md @@ -1246,7 +1246,7 @@ Behavior worth knowing: | pnpm | `pnpm-lock.yaml` (all generations), `shrinkwrap.yaml` (pnpm 1/2), Rush locks | Aliased / nested `resolution` shapes are diagnosed, not attested; `overrides` alone prove nothing | | yarn | `yarn.lock` (classic + berry) | Berry vendored entries also need the root `package.json` `resolutions` mapping; member locks are not read | | bun | `bun.lock`, else `bun.lockb` | A hosted entry that Bun < 1.3.10 re-saved without its sha512 attests only after install | -| cargo | `Cargo.lock`, `Cargo.toml`, `.cargo/config[.toml]` | Root manifest + project config only (no `$CARGO_HOME` / parent configs); a lockless hosted pin needs the redirect ledger's record | +| cargo | `Cargo.lock`, `Cargo.toml`, `.cargo/config[.toml]` | Root manifest + project config only (no `$CARGO_HOME` / parent configs); vendored `[patch.crates-io]` entries are read from `Cargo.toml` first (v5), the project config for pre-v5 projects; a manifest entry cargo ignores (a same-key project-config item, or a URL-spelled crates.io `[patch]` table) is not attested; a lockless hosted pin needs the redirect ledger's record | | golang | `go.mod`, `go.work`, `go.sum`, `go.work.sum` | A replace that `require` no longer selects is inert; `vendor/modules.txt` is not read | | pypi | `uv.lock`, `*.py.lock`, `pylock*.toml`, `poetry.lock`, `pdm.lock`, `Pipfile.lock`, `requirements.txt` (+ `-r` includes), `pyproject.toml` / `hatch.toml` | A `uv.lock` beside a `pyproject.toml` must agree with its `[tool.uv.sources]`; PDM 3.1 / 4.0–4.2 locks are refused; a Pipenv project needs `--product` (or a git remote) | | gem | `Gemfile.lock`, `gems.locked` | Platform gems unsupported; a Gemfile-only (pre-bundler-2.6, not yet locked) wiring needs the redirect ledger | diff --git a/crates/socket-patch-cli/CLI_CONTRACT.md b/crates/socket-patch-cli/CLI_CONTRACT.md index e4b72f53..68cb48c1 100644 --- a/crates/socket-patch-cli/CLI_CONTRACT.md +++ b/crates/socket-patch-cli/CLI_CONTRACT.md @@ -198,7 +198,7 @@ Discovery is read-only, never touches the network, and never fails the run: a ma | pnpm | `pnpm-lock.yaml` (every `lockfileVersion`); `shrinkwrap.yaml` only when there is no `pnpm-lock.yaml`; with `rush.json`, `common/config/rush/pnpm-lock.yaml` + `common/config/subspaces/*/pnpm-lock.yaml` | `packages:` `resolution.tarball` on the patch host | `file:.socket/vendor/npm/…` tarball + key | `integrity`, required | | yarn | `yarn.lock` (classic and berry) | classic `resolved`; berry `resolution: …::__archiveUrl=` | classic `resolved "file:./.socket/vendor/npm/…#"`; berry `file:` entry **plus** a root `package.json` `resolutions` mapping onto the same artifact (without it the entry is orphaned: diagnosed, no ref) | classic `integrity` / `#sha1`, berry `checksum`, required | | 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 | `.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. | -| 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. | `[patch.] = { path = ".socket/vendor/cargo//-" }` (config or `Cargo.toml`), live only while the lock holds a sourceless entry for it that is not in `[[patch.unused]]` | `checksum` (v1: `[metadata]`), required | +| 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. | `[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` | both go.sum lines, required. 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 `