Skip to content

Commit 8342baf

Browse files
committed
Defer pristine fetches the service makes moot
With the patch service on, `vendor` deferred the registry download of a not-installed package only for cargo; npm, golang and composer packages were downloaded and verified up front even when the service's prebuilt artifact made the pristine copy unnecessary. Those backends also ask the service first and read the pristine tree only on a local-build fallback, so their download is now deferred the same way. A package is deferred only when its fetch would really download: the fetchers' pre-download refusals (a foreign yarn berry cacheKey, a go module go fetches without a proxy, a composer entry with no dist URL) are now one shared check that both the fetch and the deferral use. pypi and gem keep the up-front fetch, which their installed-variant probe reads. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzdTEhubve9yWfqBE7vAsB
1 parent 4da7bd1 commit 8342baf

4 files changed

Lines changed: 268 additions & 17 deletions

File tree

‎crates/socket-patch-cli/CLI_CONTRACT.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ For a **9.0 root lock**, the CLI ensures `pnpm-workspace.yaml` carries `trustLoc
127127

128128
**Lockfile supplement (v3.4)**: `scan` discovery is no longer limited to installed trees. The project's lockfiles (`package-lock.json`/`npm-shrinkwrap.json`, `pnpm-lock.yaml` v9, `yarn.lock` classic + berry, `bun.lock`, `vlt-lock.json` (registry nodes, Socket-hosted pins included; vendored `file` nodes are left to the vendor ledger), `Cargo.lock`, `go.sum`, `composer.lock`, `Gemfile.lock`, `uv.lock`/`poetry.lock`/pinned `requirements.txt`) are inventoried and dependencies with NO installed copy join discovery — counts, the API lookup, the table (flagged ` [NOT INSTALLED]`, plus a stderr note), and the prune "scanned" set (a wiped node_modules no longer prunes lockfile-listed entries). JSON gains a top-level `lockfileOnlyPackages` count and an additive `notInstalled: true` on matching `packages[]` entries. `--apply` partitions lockfile-only patches out BEFORE download (calm `skipped`/`package_not_installed` records — never an error exit, never a manifest write); `--vendor` passes them through to the vendor engine's auto-fetch. Vendored-ledger entries likewise stay discoverable on a fresh clone (the committed artifact is the dependency). Global scans (`--global`) get no supplement. **Rush monorepos** (no root lockfile, `rush.json` present): the npm-lock inventory falls back to the Rush source-of-truth locks — `common/config/rush/pnpm-lock.yaml` plus every `common/config/subspaces/*/pnpm-lock.yaml` (`read_dir`-sorted, repo-relative paths preserved) — so a Rush repo's dependencies still join discovery. **Plug'n'Play layouts are an explicit refusal, not an empty inventory**: a `.pnp.*` loader means the npm packages are structurally unreachable in EVERY mode (under yarn PnP the installed-tree crawl is empty too — no `node_modules/`), so `scan` surfaces an additive top-level `warnings[]` array (`{code, detail}` objects, omitted when empty) carrying `yarn_pnp_unsupported` (same code as apply's refusal; remedy `yarn patch <pkg>`) or `pnpm_pnp_unsupported` (pnpm's `node-linker=pnp` twin; pnpm remedies), plus a stderr `Warning (<code>): …` line on the human path. Exit code and `status` are deliberately unchanged (exit 0 / `success` — the same posture as hosted refusals, which exit 0 with `redirected: 0`); the warning is the machine-readable signal that nothing was checked. Pinned by `tests/e2e_safety_yarn_pnp.rs`.
129129

130-
**Vendor auto-fetch (v3.4)**: `vendor`/`scan --vendor` no longer fail on lockfile-resolved packages with no installed copy. Already-vendored purls stage from their committed artifact (sha256-verified against the vendor ledger — a vlt directory artifact against its file inventory, which leaves out the links vlt creates inside it; offline-safe) when the ledger entry is at the manifest record's patch uuid; a superseding uuid fetches the pristine package instead, since the older artifact holds the older patch's bytes. Otherwise the pristine artifact is fetched per the lockfile resolution and verified against the lock's recorded integrity FAIL-CLOSED before any write: npm SRI (or yarn classic's sha1 fragment; for vlt the registry node's slot [2]), yarn berry's cache-zip checksum (rebuilt from the fetched tarball; cacheKey 10c0 only), Cargo.lock sha256 over the .crate, go.sum `h1:` dirhash over the module zip, composer `dist.shasum` (sha1), Gemfile.lock `CHECKSUMS` sha256, uv.lock wheel sha256 (pure `py3-none-any` wheels only). Entries the lock cannot verify are NEVER fetched (`vendor_fetch_unverifiable` warning + the calm `package_not_installed` skip). Registry bases honor `SOCKET_NPM_REGISTRY`, `SOCKET_CRATES_REGISTRY`, `SOCKET_GOPROXY` (else `GOPROXY`, `GONOPROXY` and `GOPRIVATE` the way go reads them — see the env table); npm/yarn/composer/gem/uv lock-recorded URLs are used verbatim. `--offline` refuses the fetch with the calm skip (the detail names the lockfile resolution). The fetch stages into a private tempdir — the project tree is never touched. **Deferred fetch (v5.0):** a purl the vendor ledger already covers (its entry records the record's patch uuid and the committed artifact is on disk — a file artifact only while it hashes to the ledger's `sha256`; not under `--force`), and a lockfile-only cargo crate the registry could fetch and verify (a crates.io `Cargo.lock` entry with a checksum, or the pre-vendor resolution the ledger recovers) while the patch service is enabled, are NOT downloaded up front: the fetch runs only if the backend reaches a branch that reads the pristine tree (a drifted committed copy rebuilt locally, a service miss). An in-sync re-run therefore makes no registry request, reports no `vendor_fetched_missing`, and succeeds with no network or under `--offline`. A deferred fetch that does run records its `vendor_fetched_missing` just ahead of the package's own event; one that fails, is unverifiable, or is refused by `--offline` reports the same events the up-front fetch would have. A git, path or custom-registry cargo crate is never deferred, so it keeps `vendor_fetch_unverifiable` + `package_not_installed` and is never vendored from the service's crates.io build. **Gem, local build only** (`--vendor-source build`, or no service config): a not-installed gem the lock can verify (bundler >= 2.6 `CHECKSUMS`) and no ledger entry covers is refused `gem_spec_missing` (`failed`, the backend's own detail) BEFORE any download — a downloaded `.gem` carries no eval-able stub gemspec, so a local build can never vendor it; no `vendor_fetched_missing` precedes it, and a refusal the backend would have reached first on the fetched copy reports as `gem_spec_missing` too. Not under `--dry-run`, which still fetches and previews the gem (`vendor_fetched_missing` + `verified`).
130+
**Vendor auto-fetch (v3.4)**: `vendor`/`scan --vendor` no longer fail on lockfile-resolved packages with no installed copy. Already-vendored purls stage from their committed artifact (sha256-verified against the vendor ledger — a vlt directory artifact against its file inventory, which leaves out the links vlt creates inside it; offline-safe) when the ledger entry is at the manifest record's patch uuid; a superseding uuid fetches the pristine package instead, since the older artifact holds the older patch's bytes. Otherwise the pristine artifact is fetched per the lockfile resolution and verified against the lock's recorded integrity FAIL-CLOSED before any write: npm SRI (or yarn classic's sha1 fragment; for vlt the registry node's slot [2]), yarn berry's cache-zip checksum (rebuilt from the fetched tarball; cacheKey 10c0 only), Cargo.lock sha256 over the .crate, go.sum `h1:` dirhash over the module zip, composer `dist.shasum` (sha1), Gemfile.lock `CHECKSUMS` sha256, uv.lock wheel sha256 (pure `py3-none-any` wheels only). Entries the lock cannot verify are NEVER fetched (`vendor_fetch_unverifiable` warning + the calm `package_not_installed` skip). Registry bases honor `SOCKET_NPM_REGISTRY`, `SOCKET_CRATES_REGISTRY`, `SOCKET_GOPROXY` (else `GOPROXY`, `GONOPROXY` and `GOPRIVATE` the way go reads them — see the env table); npm/yarn/composer/gem/uv lock-recorded URLs are used verbatim. `--offline` refuses the fetch with the calm skip (the detail names the lockfile resolution). The fetch stages into a private tempdir — the project tree is never touched. **Deferred fetch (v5.0):** a purl the vendor ledger already covers (its entry records the record's patch uuid and the committed artifact is on disk — a file artifact only while it hashes to the ledger's `sha256`; not under `--force`), and a lockfile-only npm, cargo, golang or composer package the registry would fetch and verify (a lock entry with an integrity, or the pre-vendor resolution the ledger recovers, that none of its fetcher's pre-download refusals applies to: a yarn berry cacheKey other than 10c0, a go module go fetches without a proxy, a composer entry with no dist URL) while the patch service is enabled, are NOT downloaded up front (those backends ask the service first and read the pristine tree only on a local-build fallback; pypi and gem keep the up-front fetch, which their installed-variant probe reads): the fetch runs only if the backend reaches a branch that reads the pristine tree (a drifted committed copy rebuilt locally, a service miss). An in-sync re-run therefore makes no registry request, reports no `vendor_fetched_missing`, and succeeds with no network or under `--offline`. A deferred fetch that does run records its `vendor_fetched_missing` just ahead of the package's own event; one that fails, is unverifiable, or is refused by `--offline` reports the same events the up-front fetch would have. A package whose fetch would be refused is never deferred (a git, path or custom-registry cargo crate, say), so it keeps `vendor_fetch_unverifiable` + `package_not_installed` and is never vendored from the service's registry build. **Gem, local build only** (`--vendor-source build`, or no service config): a not-installed gem the lock can verify (bundler >= 2.6 `CHECKSUMS`) and no ledger entry covers is refused `gem_spec_missing` (`failed`, the backend's own detail) BEFORE any download — a downloaded `.gem` carries no eval-able stub gemspec, so a local build can never vendor it; no `vendor_fetched_missing` precedes it, and a refusal the backend would have reached first on the fetched copy reports as `gem_spec_missing` too. Not under `--dry-run`, which still fetches and previews the gem (`vendor_fetched_missing` + `verified`).
131131

132132
**Vendored write durability (v5.0)**: every write is atomic (stage + rename), but only the durable commit points — lockfiles, `go.mod`/`go.sum`, `pom.xml`, `nuget.config`, `package.json`, `pnpm-workspace.yaml`, `.cargo/config.toml`, the Python/Ruby manifests, `.socket/vendor/state.json` and `redirect-state.json` — are fsynced on write. The content-verified artifacts under `.socket/vendor/<eco>/<uuid>/` (patched copies, packed/rebuilt archives and sidecars, markers) are written without an fsync and made durable by one barrier (file + directory fsync, one `F_FULLFSYNC` per device on macOS) ahead of the next commit point — and, for an artifact rebuilt in place that no commit point follows, at the end of the vendored run's commit and when the command releases the apply lock — so a crash can only lose an artifact that no durable commit point names yet, which the next run rebuilds.
133133

@@ -1315,7 +1315,7 @@ Every `--json` invocation emits a single JSON object that follows the **unified
13151315
| `vendor_override_conflict` | `failed` | vendor (pnpm/yarn-berry): a user-authored override/resolution for the package already exists. |
13161316
| `vendor_integrity_unverified` | `skipped` (warning) | vendor (pipenv): the lockfile format does not hash-check file entries; the committed wheel bytes are the protection. |
13171317
| `vendor_content_mismatch_overwritten` | `skipped` (warning) | vendor: a staged file matched NEITHER beforeHash nor afterHash (patch built against different bytes, or local edits); the stage was overwritten with the verified patched content and the vendor succeeded. |
1318-
| `vendor_fetched_missing` | `skipped` (warning) | vendor: the package was not installed; its pristine artifact was fetched per the lockfile resolution (or staged from the committed vendor artifact), integrity-verified, and vendored — the project tree was not touched. Not emitted when no fetch happened: an in-sync re-run of a ledger-covered purl, or a cargo crate the patch service served (see Vendor auto-fetch § Deferred fetch). For `poetry.lock` (which records hashes but no URLs) the pure-Python wheel's sha256 selects the file through PyPI's JSON API (`SOCKET_PYPI_JSON_API` overrides the endpoint); Poetry 0.12's bare `[metadata.hashes]` names no wheel, so those locks still need an installed copy (`vendor_fetch_unverifiable`). |
1318+
| `vendor_fetched_missing` | `skipped` (warning) | vendor: the package was not installed; its pristine artifact was fetched per the lockfile resolution (or staged from the committed vendor artifact), integrity-verified, and vendored — the project tree was not touched. Not emitted when no fetch happened: an in-sync re-run of a ledger-covered purl, or an npm, cargo, golang or composer package the patch service served (see Vendor auto-fetch § Deferred fetch). For `poetry.lock` (which records hashes but no URLs) the pure-Python wheel's sha256 selects the file through PyPI's JSON API (`SOCKET_PYPI_JSON_API` overrides the endpoint); Poetry 0.12's bare `[metadata.hashes]` names no wheel, so those locks still need an installed copy (`vendor_fetch_unverifiable`). |
13191319
| `vendor_fetch_failed` | `failed` | vendor: the lockfile-resolved fetch was attempted and failed (HTTP error, size cap, integrity mismatch, or a PRESENT-but-corrupt committed artifact — pointed at `socket-patch repair`). A MISSING committed artifact no longer lands here: it falls through to the ledger-recovered registry fetch. Suppresses the duplicate `package_not_installed` skip. |
13201320
| `vendor_fetch_unverifiable` | `skipped` (warning) | vendor: the lockfile records no usable integrity for the missing package; nothing was fetched (fail-closed) and the `package_not_installed` skip follows. Unchanged for gems by the build-mode `gem_spec_missing` refusal below, which fires only where a fetch WOULD have run. |
13211321
| `vendor_vlt_transitive_unsupported` | `failed` | vendor (vlt): the target has an inbound edge from another package in `vlt-lock.json` (the detail names it); vendored mode rewires only direct dependencies of the root or a workspace member, because vlt silently reverts transitive lock surgery. Remedy: `--mode hosted`. Refused before any download or write, dry runs included (`would_refuse`). |

‎crates/socket-patch-cli/src/commands/vendor.rs‎

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1697,6 +1697,45 @@ pub(crate) async fn pristine_fetch_is_verifiable(
16971697
}
16981698
}
16991699

1700+
/// Whether [`fetch_pristine_package`] would reach the download for this
1701+
/// purl: the same entry choice (see [`pristine_fetch_is_verifiable`]), and
1702+
/// none of the refusals its fetcher raises before the first request (a
1703+
/// foreign yarn berry cacheKey, a go module go would not fetch through a
1704+
/// proxy, a composer entry with no dist URL). Deferring a fetch that would
1705+
/// refuse `vendor_fetch_unverifiable` behind the patch service would
1706+
/// instead vendor the patch over a package it does not describe.
1707+
async fn pristine_fetch_reaches_download(
1708+
project_root: &Path,
1709+
inventory: &[lock_inventory::LockfileEntry],
1710+
purl: &str,
1711+
ledger_entry: Option<&VendorEntry>,
1712+
) -> bool {
1713+
let entry = match lock_inventory::lookup(inventory, purl)
1714+
.filter(|e| e.integrity != lock_inventory::LockIntegrity::None)
1715+
{
1716+
Some(e) => e.clone(),
1717+
None => match ledger_entry {
1718+
Some(le) => match lock_inventory::recover_lock_entry(project_root, le).await {
1719+
Ok(e) => e,
1720+
Err(_) => return false,
1721+
},
1722+
None => return false,
1723+
},
1724+
};
1725+
registry_fetch::refusal_before_download(&entry).is_none()
1726+
}
1727+
1728+
/// The ecosystems whose backend asks the patch service before it reads the
1729+
/// pristine tree, and reads it only on a local-build fallback. pypi and gem
1730+
/// read it earlier, in the loop's installed-variant probe; nuget and maven
1731+
/// have no registry fetch.
1732+
fn backend_reads_pristine_only_on_fallback(purl: &str) -> bool {
1733+
matches!(
1734+
Ecosystem::from_purl(purl),
1735+
Some(Ecosystem::Npm | Ecosystem::Cargo | Ecosystem::Golang | Ecosystem::Composer)
1736+
)
1737+
}
1738+
17001739
/// The purls among `purls` with an installed copy, found exactly as the
17011740
/// vendor loop finds them: the qualified-aware resolver
17021741
/// ([`find_packages_for_rollback_reusing`]), then the npm `package.json`
@@ -2376,11 +2415,12 @@ pub(crate) async fn vendor_records_reusing(
23762415
// backend's in-sync hot path answers it from the committed
23772416
// bytes alone, so a re-run needs no network. `--force` may
23782417
// rebuild anyway, so it keeps the eager fetch.
2379-
// * a cargo crate the patch service can serve: the backend reads
2380-
// the pristine tree only if it falls back to the local build.
2381-
// Only a crate the registry ladder COULD fetch (see
2382-
// `pristine_fetch_is_verifiable`) — a git, path or
2383-
// custom-registry crate keeps the eager rung, whose
2418+
// * a package the patch service can serve, in an ecosystem whose
2419+
// backend reads the pristine tree only if it falls back to the
2420+
// local build (`backend_reads_pristine_only_on_fallback`).
2421+
// Only one the registry ladder would really download (see
2422+
// `pristine_fetch_reaches_download`) — a git, path or
2423+
// custom-registry crate, say, keeps the eager rung, whose
23842424
// `vendor_fetch_unverifiable` refusal keeps a crates.io patch
23852425
// off it.
23862426
//
@@ -2400,10 +2440,10 @@ pub(crate) async fn vendor_records_reusing(
24002440
}
24012441
None => false,
24022442
};
2403-
let cargo_via_service = service_enabled
2443+
let via_service = service_enabled
24042444
&& matches!(rung, MissingRung::Fetch)
2405-
&& Ecosystem::from_purl(purl) == Some(Ecosystem::Cargo)
2406-
&& pristine_fetch_is_verifiable(
2445+
&& backend_reads_pristine_only_on_fallback(purl)
2446+
&& pristine_fetch_reaches_download(
24072447
&common.cwd,
24082448
inventory
24092449
.get_or_init(|| lock_inventory::inventory_project(&common.cwd))
@@ -2412,7 +2452,7 @@ pub(crate) async fn vendor_records_reusing(
24122452
lookup_entry(&state.entries, purl),
24132453
)
24142454
.await;
2415-
if covered || cargo_via_service {
2455+
if covered || via_service {
24162456
*rung = MissingRung::Deferred;
24172457
}
24182458
}

0 commit comments

Comments
 (0)