From 1e6ccc0699b3c3019fda0703a22197b9f5890c85 Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Tue, 1 Sep 2026 12:04:38 -0400 Subject: [PATCH 1/3] =?UTF-8?q?test(e2e):=20demote=20the=20cargo=20product?= =?UTF-8?q?ion=20legs=20to=20canary=20status=20=E2=80=94=20free=20cargo=20?= =?UTF-8?q?tier=20is=20unpublished?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Production deleted both pinned traitobject patches between 2026-08-27 and 2026-08-28 (0.1.1/cf2e6f58 for the hosted/vendored suites, 0.0.1/b15f2b7f for the safety round-trip), and now publishes no free-tier patch for ANY cargo crate — probes of every RUSTSEC-advisoried crate and the top 1000 crates.io packages all return empty, while the npm/pypi/gem pins remain live. With nothing to pin, the cargo install proofs cannot run; this has kept hosted-e2e and the three e2e_safety_cargo_build legs red on main since 2026-08-28. Move cargo onto the UNPUBLISHED_ECOSYSTEMS watchlist in both production suites, exactly as maven/nuget/composer are handled: the canaries keep probing production each run and, under the CANARY_STRICT knobs, nag when a free cargo patch appears again so the install proofs can be restored. Retire traitobject_real_socket_patch_round_trip (its oracle was that specific patch's compile_error!) and the cargo-only helpers nothing else references. Docs get the corrected catalog/coverage tables plus a dated demotion note; re-promotion is the existing withdrawn-patch procedure with this commit's history as the restore template. Verified: safety suite 5/5; hosted suite 15/15 live against production under SOCKET_PATCH_HOSTED_E2E_STRICT=1; vendored suite 11/12 (the one red is pre-existing server-side gem-catalog drift, untouched by this change); CI's clippy invocation clean; no traitobject reference remains outside get.rs's mocked unit tests. Co-Authored-By: Claude Fable 5 --- .../tests/e2e_hosted_production.rs | 163 +++---------- .../tests/e2e_safety_cargo_build.rs | 213 +---------------- .../tests/e2e_vendored_production.rs | 222 ++---------------- docs/testing/hosted-production-e2e.md | 41 ++-- docs/testing/vendored-production-e2e.md | 33 +-- 5 files changed, 100 insertions(+), 572 deletions(-) diff --git a/crates/socket-patch-cli/tests/e2e_hosted_production.rs b/crates/socket-patch-cli/tests/e2e_hosted_production.rs index 29002aa7..312cdcbd 100644 --- a/crates/socket-patch-cli/tests/e2e_hosted_production.rs +++ b/crates/socket-patch-cli/tests/e2e_hosted_production.rs @@ -35,7 +35,6 @@ //! |-----------|------|------------|----------| //! | npm | `pkg:npm/minimist@1.2.2` | `80630680-4da6-45f9-bba8-b888e0ffd58c` | GHSA-xvch-5gv4-984h (CVE-2021-44906) | //! | PyPI | `pkg:pypi/urllib3@1.26.18` | *any of three* (see [`PYPI_UUIDS`]) | GHSA-gm62-xv2j-4w53 &co | -//! | Cargo | `pkg:cargo/traitobject@0.1.1` | `cf2e6f58-d9fa-4096-9151-c34afa717f89` | GHSA-pp8r-vv2j-9j5v | //! | gem | `pkg:gem/activestorage@6.0.3` | *any of* [`GEM_UUIDS`] (five today) | GHSA-m42x-37p3-fv5w (CVE-2020-8162), GHSA-w749-p3v6-hccq (CVE-2022-21831), GHSA-9xrj-h377-fr87 (CVE-2026-33195), GHSA-r4mg-4433-c7g3 (CVE-2025-24293), GHSA-xr9x-r78c-5hrm (CVE-2026-66066) | //! //! `docs/testing/hosted-production-e2e.md` explains how these were chosen and @@ -43,11 +42,14 @@ //! //! # Ecosystems with no coverage, and why //! -//! * **maven / nuget / composer** — hosted mode is implemented and documented -//! for all three, but production currently publishes **zero** free-tier -//! patches for them, so there is nothing real to redirect to. Rather than -//! silently skipping, [`canary_unpublished_ecosystems`] probes production -//! every run and tells us the moment that changes. +//! * **cargo / maven / nuget / composer** — hosted mode is implemented and +//! documented for all four, but production currently publishes **zero** +//! free-tier patches for them, so there is nothing real to redirect to. +//! Rather than silently skipping, [`canary_unpublished_ecosystems`] probes +//! production every run and tells us the moment that changes. cargo carried +//! a full sparse-registry install proof until 2026-09-01: production's free +//! cargo tier emptied on 2026-08-28, so the leg was demoted to the canary +//! (`docs/testing/hosted-production-e2e.md` says how to re-promote it). //! * **golang** — hosted mode is supported for free-tier references carrying //! a `goproxy` override (`docs/design/golang-hosted.md`), but production //! publishes no golang hosted modules yet. Covered as a shape guard that @@ -58,11 +60,11 @@ //! //! Toolchains (each leg soft-skips if its own toolchain is absent, unless //! `SOCKET_PATCH_HOSTED_E2E_STRICT=1`): `npm`, `pnpm`, `yarn` (classic), -//! `corepack` (berry), `bun`, `uv`, `cargo`, `ruby` + `bundle`, `go`. +//! `corepack` (berry), `bun`, `uv`, `ruby` + `bundle`, `go`. //! //! Network egress to: `patches-api.socket.dev`, `patch.socket.dev`, //! `registry.npmjs.org`, `pypi.org`, `files.pythonhosted.org`, -//! `static.crates.io`, `index.crates.io`, `rubygems.org`. +//! `rubygems.org`. //! //! No API token is used or needed — the suite deliberately runs against the //! **free public proxy**, which is the surface every unauthenticated user @@ -118,16 +120,6 @@ const PYPI_UUIDS: &[&str] = &[ "e828efa5-5c6d-43f3-9909-03f5ac232b98", ]; -const CARGO_PURL: &str = "pkg:cargo/traitobject@0.1.1"; -const CARGO_NAME: &str = "traitobject"; -const CARGO_VERSION: &str = "0.1.1"; -const CARGO_UUID: &str = "cf2e6f58-d9fa-4096-9151-c34afa717f89"; -/// The traitobject patch annotates `src/lib.rs` with its advisory ID (the -/// crate is unmaintained; the patch documents that and fixes deprecations). -/// Cargo crates are not rewritten with the `// Socket Community Patch` header -/// that npm/PyPI artifacts carry, so this is the marker to look for. -const CARGO_MARKER: &str = "GHSA-pp8r-vv2j-9j5v"; - /// The gem pin is deliberately UNQUALIFIED. Production publishes the purl as /// `pkg:gem/activestorage@6.0.3?platform=ruby`, but nothing client-side /// strips qualifiers — the SERVER normalizes both spellings to the same @@ -182,6 +174,19 @@ const PATCH_MARKER: &str = "Socket Community Patch"; /// patches to exercise it with. [`canary_unpublished_ecosystems`] watches /// these so coverage can be extended the moment one lights up. const UNPUBLISHED_ECOSYSTEMS: &[(&str, &[&str])] = &[ + // cargo joined this list on 2026-09-01: production deleted its last free + // cargo patches on 2026-08-28, retiring the pinned sparse-registry + // install proof this suite used to carry. Re-promotion procedure: + // docs/testing/hosted-production-e2e.md. + ( + "cargo", + &[ + "pkg:cargo/openssl", + "pkg:cargo/tokio", + "pkg:cargo/hyper", + "pkg:cargo/smallvec", + ], + ), ( "maven", &[ @@ -729,7 +734,6 @@ async fn preflight_required_patches_are_published() { let required: Vec<(&str, Vec<&str>)> = vec![ (NPM_PURL, vec![NPM_UUID]), (PYPI_PURL, PYPI_UUIDS.to_vec()), - (CARGO_PURL, vec![CARGO_UUID]), (GEM_PURL, GEM_UUIDS.to_vec()), ]; @@ -783,7 +787,7 @@ async fn canary_patches_name_advisories_so_merge_state_is_inferable() { let mut failures: Vec = Vec::new(); let mut coverage_seen: Vec<(String, String, usize)> = Vec::new(); - let canary_purls = vec![NPM_PURL, PYPI_PURL, CARGO_PURL, GEM_PURL]; + let canary_purls = vec![NPM_PURL, PYPI_PURL, GEM_PURL]; for purl in canary_purls { match published_patch_advisory_counts(purl).await { Err(e) => failures.push(format!("{purl}: production probe failed: {e}")), @@ -1531,111 +1535,6 @@ fn pypi_uv_lock_hosted_install_proof() { ); } -// =========================================================================== -// Cargo — per-patch sparse registry -// =========================================================================== - -#[test] -#[ignore = "live production API + real crates.io. Run with --ignored."] -fn cargo_hosted_install_proof() { - const LEG: &str = "cargo_hosted_install_proof"; - if !has_command("cargo") { - soft_skip!(LEG, "`cargo` not on PATH"); - } - let tmp = tempfile::tempdir().expect("tempdir"); - let proj = tmp.path().join("proj"); - std::fs::create_dir_all(proj.join("src")).expect("mkdir src"); - let home = tmp.path().join("cargo-home").display().to_string(); - let env = [("CARGO_HOME", home.as_str())]; - - std::fs::write( - proj.join("Cargo.toml"), - format!( - "[package]\nname = \"hosted-e2e\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n\ - [dependencies]\n{CARGO_NAME} = \"={CARGO_VERSION}\"\n" - ), - ) - .expect("write Cargo.toml"); - std::fs::write(proj.join("src").join("main.rs"), "fn main() {}\n").expect("write main.rs"); - - let fetch = tool(&proj, "cargo", &["fetch"], &env); - if !ok(&fetch) { - soft_skip!(LEG, "upstream `cargo fetch` failed:\n{}", dump(&fetch)); - } - let pristine_lock = read(&proj.join("Cargo.lock")); - assert!( - pristine_lock.contains("registry+https://github.com/rust-lang/crates.io-index"), - "{LEG}: pristine Cargo.lock does not resolve {CARGO_NAME} from \ - crates.io — fixture setup is wrong:\n{pristine_lock}" - ); - - let env_json = scan_hosted(&proj, &[]); - assert_redirected(&env_json, "Cargo.lock"); - - let lock = read(&proj.join("Cargo.lock")); - assert_hosted_pin(&lock, &[CARGO_UUID], LEG); - let config = read(&proj.join(".cargo").join("config.toml")); - assert!( - config.contains(&format!( - "sparse+https://{PATCH_HOST}/patch-registry/cargo/" - )), - "{LEG}: .cargo/config.toml declares no Socket sparse registry:\n{config}" - ); - let manifest = read(&proj.join("Cargo.toml")); - assert!( - manifest.contains(&format!("socket-patch-{CARGO_UUID}")), - "{LEG}: Cargo.toml does not route {CARGO_NAME} at the per-patch \ - registry:\n{manifest}" - ); - - // Proof: fetch again with a cold CARGO_HOME so cargo must reach the Socket - // sparse index, download the crate, and verify the checksum in the lock. - let cold = tmp.path().join("cargo-home-cold").display().to_string(); - let cold_env = [("CARGO_HOME", cold.as_str())]; - let refetch = tool(&proj, "cargo", &["fetch"], &cold_env); - assert!( - ok(&refetch), - "{LEG}: `cargo fetch` from the Socket sparse registry failed — cargo \ - could not reach the index, download the crate, or verify its \ - checksum:\n{}", - dump(&refetch) - ); - - // The extracted source must be the patched crate, not the crates.io one. - let src_root = Path::new(&cold).join("registry").join("src"); - let mut found = None; - if let Ok(hosts) = std::fs::read_dir(&src_root) { - for host in hosts.flatten() { - let candidate = host - .path() - .join(format!("{CARGO_NAME}-{CARGO_VERSION}")) - .join("src") - .join("lib.rs"); - if candidate.exists() { - found = Some(candidate); - break; - } - } - } - let lib_rs = found.unwrap_or_else(|| { - panic!("{LEG}: no extracted {CARGO_NAME}-{CARGO_VERSION}/src/lib.rs under {src_root:?}") - }); - assert!( - lib_rs - .parent() - .and_then(|p| p.parent()) - .and_then(|p| p.parent()) - .and_then(|p| p.file_name()) - .map(|n| n.to_string_lossy().contains(PATCH_HOST)) - .unwrap_or(false), - "{LEG}: {CARGO_NAME} was extracted from a non-Socket registry dir \ - ({}) — cargo served it from the crates.io cache instead of the \ - redirect", - lib_rs.display() - ); - assert_patched(&lib_rs, CARGO_MARKER, LEG); -} - // =========================================================================== // RubyGems — full hosted install proof // =========================================================================== @@ -1960,10 +1859,12 @@ fn deno_hosted_is_unsupported() { // Canary — ecosystems whose hosted support has nothing to test against // =========================================================================== -/// maven, nuget and composer all implement hosted mode, but production +/// cargo, maven, nuget and composer all implement hosted mode, but production /// publishes no free-tier patches for them, so there is no honest end-to-end -/// leg to write. This probes production every run and reports the moment that -/// changes, so coverage can be extended deliberately rather than by accident. +/// leg to write. (cargo used to have one — the pinned sparse-registry install +/// proof retired 2026-09-01 when production's free cargo tier emptied.) This +/// probes production every run and reports the moment that changes, so +/// coverage can be extended deliberately rather than by accident. /// /// It deliberately does NOT fail when patches appear: production publishing a /// new patch is not a socket-patch regression, and a required check must not @@ -1996,9 +1897,9 @@ async fn canary_unpublished_ecosystems() { if newly_published.is_empty() { println!( - "canary_unpublished_ecosystems: maven / nuget / composer still have \ - no free-tier published patches — their hosted-mode legs remain \ - untestable end-to-end against production." + "canary_unpublished_ecosystems: cargo / maven / nuget / composer \ + still have no free-tier published patches — their hosted-mode legs \ + remain untestable end-to-end against production." ); return; } diff --git a/crates/socket-patch-cli/tests/e2e_safety_cargo_build.rs b/crates/socket-patch-cli/tests/e2e_safety_cargo_build.rs index 06e0a779..d766c647 100644 --- a/crates/socket-patch-cli/tests/e2e_safety_cargo_build.rs +++ b/crates/socket-patch-cli/tests/e2e_safety_cargo_build.rs @@ -47,8 +47,8 @@ use sha2::{Digest, Sha256}; mod common; use common::{ - assert_run_ok, cargo_run, has_command, parse_json_envelope, run, run_with_env, sha256_hex, - write_blob, write_minimal_manifest, PatchEntry, + assert_run_ok, cargo_run, has_command, parse_json_envelope, run, sha256_hex, write_blob, + write_minimal_manifest, PatchEntry, }; const ORIGINAL_LIB_RS: &str = "pub fn hello() -> &'static str { \"world\" }\n"; @@ -996,212 +996,3 @@ fn apply_normalizes_package_prefix_in_cargo_checksum() { "sidecar record must still report .cargo-checksum.json:rewritten; got {cargo}" ); } - -/// Headline real-world round trip: fetch the actual `traitobject@0.0.1` -/// crate from crates.io, apply the real Socket patch -/// `b15f2b7f-d5cb-43c9-b793-80f71682188f` from the public proxy, then -/// run `cargo check` against a consumer that depends on it. -/// -/// This is the cargo "layer 2 + layer 3" combined test (per the -/// PR #80 plan): a real published crate plus the real Socket patch, -/// no synthetic fixtures. Proves the sidecar fixup composes with -/// cargo's actual on-disk verification of crates.io sources. -/// -/// Network deps: -/// - crates.io (cargo fetch traitobject@0.0.1) -/// - patches-api.socket.dev (socket-patch get, public proxy) -/// -/// The traitobject 0.0.1 patch adds a `compile_error!` to `src/lib.rs` -/// gated on `#[cfg(not(feature = "allow-unmaintained"))]`, and adds -/// that feature to the crate's Cargo.toml. The consumer MUST declare -/// the dep bare: cargo resolves features from the crates.io INDEX, -/// which knows nothing of the patch-added feature, so declaring -/// `features = ["allow-unmaintained"]` fails resolution ("does not -/// have that feature") before fetch even runs — against both the -/// unpatched AND the patched crate. (This test originally shipped -/// with the feature declared; the resulting resolution error was -/// swallowed by the fetch skip path and the test silently skipped -/// itself on every machine.) The end-to-end oracle is therefore the -/// patch's own compile_error: cargo accepting the rewritten checksums -/// and then failing compilation with the "unmaintained" message -/// proves the sidecar fixup landed AND that rustc consumed the -/// patched bytes. -#[test] -#[ignore] -fn traitobject_real_socket_patch_round_trip() { - if !has_command("cargo") { - eprintln!("SKIP: cargo not on PATH"); - return; - } - let root = tempfile::tempdir().unwrap(); - let consumer = root.path().join("consumer"); - let cargo_home = root.path().join(".cargo-home"); - std::fs::create_dir_all(consumer.join("src")).unwrap(); - - // Consumer crate that uses traitobject, declared bare (see the - // doc comment: the patch-added feature is index-invisible and can - // never be enabled on a registry dependency). - std::fs::write( - consumer.join("Cargo.toml"), - r#"[package] -name = "traitobject-consumer" -version = "0.0.1" -edition = "2021" - -[dependencies] -traitobject = "0.0.1" -"#, - ) - .unwrap(); - std::fs::write(consumer.join("src/main.rs"), "fn main() {}\n").unwrap(); - - // 1. Fetch traitobject@0.0.1 from crates.io (real network). - // Hermetic CARGO_HOME means we never touch the user's cache. - let cargo_home_str = cargo_home.to_str().unwrap(); - let fetch = Command::new("cargo") - .args(["fetch"]) - .current_dir(&consumer) - .env("CARGO_HOME", cargo_home_str) - .output() - .expect("cargo fetch"); - if !fetch.status.success() { - // Network unavailable, crates.io down, etc. — skip rather - // than fail. The ignore gate already keeps us out of the - // default test run; this is a defensive second skip path. - eprintln!( - "SKIP: cargo fetch traitobject failed (likely network):\nstdout:\n{}\nstderr:\n{}", - String::from_utf8_lossy(&fetch.stdout), - String::from_utf8_lossy(&fetch.stderr), - ); - return; - } - - // 2. Confirm the unpacked source landed under the registry path. - // Shape: `/registry/src/index.crates.io-*/traitobject-0.0.1/`. - let registry_src = cargo_home.join("registry/src"); - let mut traitobject_dir: Option = None; - for entry in std::fs::read_dir(®istry_src).unwrap() { - let entry = entry.unwrap(); - let candidate = entry.path().join("traitobject-0.0.1"); - if candidate.is_dir() { - traitobject_dir = Some(candidate); - break; - } - } - let traitobject_dir = traitobject_dir - .expect("traitobject-0.0.1 should be unpacked under cargo registry/src after cargo fetch"); - // Modern cargo no longer materializes `.cargo-checksum.json` under - // registry/src — it writes a bare `.cargo-ok` marker and verifies the - // .crate tarball at unpack time, leaving no per-file hashes for the - // sidecar to fix up (fixup correctly returns "nothing to do"). Older - // toolchains DO ship the file; snapshot conditionally so the rewrite - // assertions still fire wherever the file exists. - let checksum_path = traitobject_dir.join(".cargo-checksum.json"); - let pre_apply_checksum: Option = std::fs::read_to_string(&checksum_path) - .ok() - .map(|raw| serde_json::from_str(&raw).expect("pre-apply .cargo-checksum.json must parse")); - - // 3. Run `socket-patch get` against the public proxy. This - // downloads + applies the real patch in one shot. Route through - // the scrubbed runner so ambient SOCKET_* (DRY_RUN, MANIFEST_PATH, - // GLOBAL, ...) can't no-op or redirect the apply; the scrub also - // strips SOCKET_API_TOKEN, forcing the public proxy. - let (get_code, get_stdout, get_stderr) = run_with_env( - &consumer, - &[ - "get", - "b15f2b7f-d5cb-43c9-b793-80f71682188f", - "--cwd", - consumer.to_str().unwrap(), - ], - &[("CARGO_HOME", cargo_home_str)], - ); - if get_code != 0 { - eprintln!( - "SKIP: socket-patch get failed (likely network):\nstdout:\n{get_stdout}\nstderr:\n{get_stderr}" - ); - return; - } - - // 4. Manifest should now record the patch. - let manifest_path = consumer.join(".socket/manifest.json"); - let manifest: serde_json::Value = serde_json::from_str( - &std::fs::read_to_string(&manifest_path).expect("manifest.json must exist after get"), - ) - .unwrap(); - let patch = &manifest["patches"]["pkg:cargo/traitobject@0.0.1"]; - assert!( - patch.is_object(), - "manifest should contain the traitobject patch: {manifest}" - ); - - // 5. Where a `.cargo-checksum.json` existed, the sidecar fixup must - // have rewritten it: the patch covers src/lib.rs (and Cargo.toml, - // Cargo.lock, README.md), so those entries should have NEW SHA256 - // values while every unpatched-file entry stays put. Where cargo - // never wrote one (modern toolchains), the sidecar must not - // invent one. - match &pre_apply_checksum { - Some(pre) => { - let post_apply_checksum: serde_json::Value = serde_json::from_str( - &std::fs::read_to_string(&checksum_path) - .expect("sidecar must not delete .cargo-checksum.json"), - ) - .unwrap(); - let pre_files = pre["files"].as_object().unwrap(); - let post_files = post_apply_checksum["files"].as_object().unwrap(); - let patched_paths = ["Cargo.toml", "Cargo.lock", "README.md", "src/lib.rs"]; - for f in patched_paths { - if let (Some(pre), Some(post)) = (pre_files.get(f), post_files.get(f)) { - assert_ne!( - pre, post, - ".cargo-checksum.json entry for {f} should change after apply" - ); - assert_eq!( - post.as_str().unwrap().len(), - 64, - "post-apply hash for {f} should be 64-hex SHA256" - ); - } - } - // `package` field is preserved (the .crate tarball hash didn't - // become honestly recomputable without the original .crate). - assert_eq!( - pre["package"], post_apply_checksum["package"], - ".cargo-checksum.json `package` field must survive the rewrite unchanged" - ); - } - None => { - assert!( - !checksum_path.exists(), - "apply must not create a .cargo-checksum.json cargo never wrote" - ); - } - } - - // 6. The whole point: cargo gets PAST checksum verification and - // compiles the PATCHED source. A green build is impossible here - // by the patch's design (the compile_error can only be silenced - // by a feature no registry consumer can enable), so the honest - // oracle is two-sided: - // * no "checksum ... has changed" rejection — the sidecar - // rewrite was accepted by cargo's registry-source - // verification, and - // * the patch's own compile_error text in stderr — the bytes - // rustc consumed are the patched bytes, not the originals - // (unpatched traitobject compiles clean, so a silent - // no-op apply would turn this check green and fail below). - let check = cargo_check(&consumer, &cargo_home); - let check_stderr = String::from_utf8_lossy(&check.stderr); - assert!( - !(check_stderr.contains("checksum") && check_stderr.contains("changed")), - "cargo must accept the sidecar-rewritten checksums, not reject them:\n{check_stderr}" - ); - assert!( - !check.status.success() && check_stderr.contains("unmaintained"), - "cargo check must fail with the patch's `unmaintained` compile_error \ - (proving the patched source compiled); a success here means the \ - patch never reached the compiled bytes.\nstdout:\n{}\nstderr:\n{check_stderr}", - String::from_utf8_lossy(&check.stdout), - ); -} diff --git a/crates/socket-patch-cli/tests/e2e_vendored_production.rs b/crates/socket-patch-cli/tests/e2e_vendored_production.rs index 42472402..74f39e14 100644 --- a/crates/socket-patch-cli/tests/e2e_vendored_production.rs +++ b/crates/socket-patch-cli/tests/e2e_vendored_production.rs @@ -51,7 +51,6 @@ //! |-----------|------|------------|-----------------------------| //! | npm | `pkg:npm/minimist@1.2.2` | `80630680-4da6-45f9-bba8-b888e0ffd58c` | `Socket Community Patch` header | //! | PyPI | `pkg:pypi/urllib3@1.26.18` | *any of three* (see [`PYPI_UUIDS`]) | `Socket Community Patch` header | -//! | Cargo | `pkg:cargo/traitobject@0.1.1` | `cf2e6f58-d9fa-4096-9151-c34afa717f89` | advisory id `GHSA-pp8r-vv2j-9j5v` | //! | gem | `pkg:gem/activestorage@6.0.3` | *any of* [`GEM_PATCHES`] | `Socket Community Patch` header | //! //! # Ecosystem coverage notes (gaps, and one resolved gap) @@ -72,10 +71,13 @@ //! publishes no free golang patches, so there is nothing to vendor. //! [`golang_vendored_finds_no_free_patches`] asserts exactly that (zero //! applied) and would light up the moment a golang patch is published. -//! * **maven / nuget / composer** — vendored mode is implemented, but +//! * **cargo / maven / nuget / composer** — vendored mode is implemented, but //! production publishes **zero** free-tier patches for them. //! [`canary_unpublished_vendored_ecosystems`] probes production every run and -//! reports the moment that changes. +//! reports the moment that changes. cargo carried a full +//! `[patch.crates-io]` delivery proof until 2026-09-01: production's free +//! cargo tier emptied on 2026-08-28, so the leg was demoted to the canary +//! (`docs/testing/vendored-production-e2e.md` says how to re-promote it). //! * **deno** — vendored mode is not supported. //! [`deno_vendored_is_unsupported`] covers it as a negative assertion. //! @@ -83,12 +85,11 @@ //! //! Toolchains (each leg soft-skips if its own toolchain is absent, unless //! `SOCKET_PATCH_VENDORED_E2E_STRICT=1`): `npm`, `pnpm`, `corepack` (yarn -//! classic + berry), `bun`, `uv`, `python3` (pip), `cargo`, `ruby` + `bundle`, -//! `go`. +//! classic + berry), `bun`, `uv`, `python3` (pip), `ruby` + `bundle`, `go`. //! //! Network egress to: `patches-api.socket.dev`, `patch.socket.dev`, //! `registry.npmjs.org`, `pypi.org`, `files.pythonhosted.org`, -//! `static.crates.io`, `index.crates.io`, `rubygems.org`. +//! `rubygems.org`. //! //! No API token is used or needed — the suite deliberately runs against the //! **free public proxy** with `SOCKET_NO_CONFIG=true` so a developer's @@ -139,18 +140,6 @@ const PYPI_UUIDS: &[&str] = &[ "e828efa5-5c6d-43f3-9909-03f5ac232b98", ]; -const CARGO_PURL: &str = "pkg:cargo/traitobject@0.1.1"; -const CARGO_NAME: &str = "traitobject"; -const CARGO_VERSION: &str = "0.1.1"; -const CARGO_UUID: &str = "cf2e6f58-d9fa-4096-9151-c34afa717f89"; -/// The traitobject patch annotates `src/lib.rs` with its advisory ID. Cargo -/// crates are not rewritten with the `Socket Community Patch` header the -/// npm/PyPI artifacts carry, so this is the marker to look for. (The same -/// patch also injects a `compile_error!` unless the `allow-unmaintained` -/// feature is set — which is why the cargo delivery proof uses `cargo fetch`, -/// not `cargo build`; see [`cargo_vendored_install_proof`].) -const CARGO_MARKER: &str = "GHSA-pp8r-vv2j-9j5v"; - /// The gem pin is deliberately UNQUALIFIED. Production publishes the purl as /// `pkg:gem/activestorage@6.0.3?platform=ruby`, but nothing client-side /// strips qualifiers — the SERVER normalizes both spellings to the same @@ -207,6 +196,14 @@ const YARN_BERRY: &str = "yarn@4.6.0"; /// patches to exercise it with. [`canary_unpublished_vendored_ecosystems`] /// watches these so coverage can be extended the moment one lights up. const UNPUBLISHED_ECOSYSTEMS: &[(&str, &[&str])] = &[ + // cargo joined this list on 2026-09-01: production deleted its last free + // cargo patches on 2026-08-28, retiring the pinned `[patch.crates-io]` + // delivery proof this suite used to carry. Re-promotion procedure: + // docs/testing/vendored-production-e2e.md. + ( + "cargo", + &["pkg:cargo/openssl", "pkg:cargo/tokio", "pkg:cargo/smallvec"], + ), ( "maven", &[ @@ -738,7 +735,6 @@ async fn preflight_required_patches_are_published() { let required: Vec<(&str, Vec<&str>)> = vec![ (NPM_PURL, vec![NPM_UUID]), (PYPI_PURL, PYPI_UUIDS.to_vec()), - (CARGO_PURL, vec![CARGO_UUID]), (GEM_PURL, GEM_PATCHES.iter().map(|(u, _)| *u).collect()), ]; @@ -1599,180 +1595,6 @@ fn pypi_uv_lock_vendored_install_proof() { ); } -// =========================================================================== -// Cargo — `[patch.crates-io]` path dep -// =========================================================================== - -/// Cargo's vendored artifact is a **directory** (a `[patch.crates-io]` path -/// dep), so the delivery proof is a `cargo fetch --offline` that resolves the -/// whole dependency graph from committable files with an empty CARGO_HOME — -/// proving zero registry access — plus a byte check that the vendored -/// directory carries the patch. -/// -/// It deliberately does NOT `cargo build`: the production traitobject patch -/// injects a `compile_error!` unless the `allow-unmaintained` Cargo feature is -/// enabled (the patch's whole point is to make the unmaintained crate refuse to -/// compile silently). That is patch *content*, not a vendoring defect, and this -/// suite proves byte delivery, not CVE efficacy. -#[test] -#[ignore = "live production API + real crates.io. Run with --ignored."] -fn cargo_vendored_install_proof() { - const LEG: &str = "cargo_vendored_install_proof"; - if !has_command("cargo") { - soft_skip!(LEG, "`cargo` not on PATH"); - } - let tmp = tempfile::tempdir().expect("tempdir"); - let proj = tmp.path().join("proj"); - std::fs::create_dir_all(proj.join("src")).expect("mkdir src"); - let home = tmp.path().join("cargo-home").display().to_string(); - let env = [("CARGO_HOME", home.as_str())]; - - std::fs::write( - proj.join("Cargo.toml"), - format!( - "[package]\nname = \"vendored-e2e\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n\ - [dependencies]\n{CARGO_NAME} = \"={CARGO_VERSION}\"\n" - ), - ) - .expect("write Cargo.toml"); - std::fs::write(proj.join("src").join("main.rs"), "fn main() {}\n").expect("write main.rs"); - - let fetch = tool(&proj, "cargo", &["fetch"], &env); - if !ok(&fetch) { - soft_skip!(LEG, "upstream `cargo fetch` failed:\n{}", dump(&fetch)); - } - let pristine_lock = std::fs::read(proj.join("Cargo.lock")).unwrap(); - - // The pristine registry-extracted source must NOT carry the marker. - let registry_lib = find_registry_lib(Path::new(&home)); - if let Some(ref lib) = registry_lib { - assert_pristine(lib, CARGO_MARKER, LEG); - } - - let env_json = scan_vendored(&proj, &[]); - assert_vendor_applied(&env_json, CARGO_PURL, LEG); - assert_download_uuid(&env_json, &[CARGO_UUID], LEG); - - // Vendored directory carries the patch; the registry source stays pristine. - let vendored_lib = proj - .join(format!( - ".socket/vendor/cargo/{CARGO_UUID}/{CARGO_NAME}-{CARGO_VERSION}" - )) - .join("src/lib.rs"); - assert_patched(&vendored_lib, CARGO_MARKER, LEG); - if let Some(ref lib) = registry_lib { - assert!( - !read(lib).contains(CARGO_MARKER), - "{LEG}: vendoring mutated the pristine registry source at {} — vendor must copy, \ - never mutate", - lib.display() - ); - } - let config = read(&proj.join(".cargo/config.toml")); - assert!( - config.contains("[patch.crates-io]") - && config.contains(&format!(".socket/vendor/cargo/{CARGO_UUID}/")), - "{LEG}: .cargo/config.toml declares no [patch.crates-io] pointing at the vendored \ - crate:\n{config}" - ); - // Lock detached from the registry (keeps name+version, loses source+checksum). - let lock = read(&proj.join("Cargo.lock")); - let block = cargo_package_block(&lock, CARGO_NAME).expect("traitobject lock entry survives"); - assert!( - !block.contains("source = ") && !block.contains("checksum = "), - "{LEG}: lock entry must be detached from the registry:\n{block}" - ); - let lock_wired = std::fs::read(proj.join("Cargo.lock")).unwrap(); - - // DELIVERY PROOF: committable files only, EMPTY CARGO_HOME, `cargo fetch - // --offline --locked` — resolves the whole graph from the vendored path - // with zero registry downloads. - let fresh = tmp.path().join("fresh"); - std::fs::create_dir_all(&fresh).unwrap(); - std::fs::copy(proj.join("Cargo.toml"), fresh.join("Cargo.toml")).unwrap(); - std::fs::copy(proj.join("Cargo.lock"), fresh.join("Cargo.lock")).unwrap(); - copy_dir_recursive(&proj.join(".cargo"), &fresh.join(".cargo")); - copy_dir_recursive(&proj.join("src"), &fresh.join("src")); - copy_dir_recursive(&proj.join(".socket"), &fresh.join(".socket")); - let fresh_home = tmp.path().join("fresh-cargo-home"); - std::fs::create_dir_all(&fresh_home).unwrap(); - let fresh_home_s = fresh_home.display().to_string(); - let refetch = tool( - &fresh, - "cargo", - &["fetch", "--offline", "--locked"], - &[("CARGO_HOME", fresh_home_s.as_str())], - ); - assert!( - ok(&refetch), - "{LEG}: `cargo fetch --offline --locked` from the vendored path (empty CARGO_HOME) \ - failed — cargo could not resolve the graph from committable files alone:\n{}", - dump(&refetch) - ); - assert!( - !fresh_home.join("registry").exists(), - "{LEG}: the empty CARGO_HOME gained a registry/ — cargo hit the network instead of \ - resolving {CARGO_NAME} from the vendored path dep" - ); - // The delivered bytes ARE the vendored directory's bytes (path dep), and - // they carry the patch marker. - assert_patched( - &fresh.join(format!( - ".socket/vendor/cargo/{CARGO_UUID}/{CARGO_NAME}-{CARGO_VERSION}/src/lib.rs" - )), - CARGO_MARKER, - LEG, - ); - - // Idempotency + revert. - let env2 = scan_vendored(&proj, &[]); - assert_eq!( - env2["vendor"]["summary"]["applied"].as_u64().unwrap_or(99), - 0, - "{LEG}: re-run must vendor nothing new:\n{env2:#}" - ); - assert_eq!( - std::fs::read(proj.join("Cargo.lock")).unwrap(), - lock_wired, - "{LEG}: re-run must leave Cargo.lock byte-identical" - ); - assert_eq!(vendor_revert(&proj, LEG), 1, "{LEG}: one entry reverted"); - assert_eq!( - std::fs::read(proj.join("Cargo.lock")).unwrap(), - pristine_lock, - "{LEG}: revert must restore Cargo.lock byte-identical" - ); - assert!( - !proj.join(".socket/vendor").exists(), - "{LEG}: .socket/vendor must be gone after revert" - ); -} - -/// Find `/registry/src//traitobject-0.1.1/src/lib.rs`. -fn find_registry_lib(cargo_home: &Path) -> Option { - let src = cargo_home.join("registry").join("src"); - for host in std::fs::read_dir(&src).ok()?.flatten() { - let candidate = host - .path() - .join(format!("{CARGO_NAME}-{CARGO_VERSION}")) - .join("src") - .join("lib.rs"); - if candidate.exists() { - return Some(candidate); - } - } - None -} - -/// The full `[[package]]` block (text) for `name` in Cargo.lock. -fn cargo_package_block(lock_text: &str, name: &str) -> Option { - let needle = format!("name = \"{name}\""); - lock_text - .split("[[package]]") - .find(|block| block.lines().any(|l| l.trim() == needle)) - .map(str::to_string) -} - // =========================================================================== // RubyGems — bundler `path:` source // =========================================================================== @@ -2147,10 +1969,12 @@ fn deno_vendored_is_unsupported() { ); } -/// maven, nuget and composer all implement vendored mode, but production -/// publishes no free-tier patches for them. This probes production every run -/// and reports the moment that changes, so coverage can be extended -/// deliberately rather than by accident. It does not fail when patches appear; +/// cargo, maven, nuget and composer all implement vendored mode, but +/// production publishes no free-tier patches for them. (cargo used to have a +/// full delivery proof — retired 2026-09-01 when production's free cargo tier +/// emptied.) This probes production every run and reports the moment that +/// changes, so coverage can be extended deliberately rather than by accident. +/// It does not fail when patches appear; /// `SOCKET_PATCH_VENDORED_E2E_CANARY_STRICT=1` makes it fail, for a scheduled /// nag run. #[tokio::test(flavor = "multi_thread")] @@ -2180,8 +2004,8 @@ async fn canary_unpublished_vendored_ecosystems() { if newly_published.is_empty() { println!( - "canary_unpublished_vendored_ecosystems: maven / nuget / composer still have no \ - free-tier published patches — their vendored-mode legs remain untestable \ + "canary_unpublished_vendored_ecosystems: cargo / maven / nuget / composer still \ + have no free-tier published patches — their vendored-mode legs remain untestable \ end-to-end against production." ); return; diff --git a/docs/testing/hosted-production-e2e.md b/docs/testing/hosted-production-e2e.md index 447d428a..5d48a530 100644 --- a/docs/testing/hosted-production-e2e.md +++ b/docs/testing/hosted-production-e2e.md @@ -40,7 +40,6 @@ from the child environment. |-----------|------|------------|----------|---------| | npm | `pkg:npm/minimist@1.2.2` | `80630680-4da6-45f9-bba8-b888e0ffd58c` | GHSA-xvch-5gv4-984h / CVE-2021-44906 | all five npm-family legs | | PyPI | `pkg:pypi/urllib3@1.26.18` | `de58c8b8-796c-4b6d-8a48-539b5563db76`, `26242e35-f867-4da8-8789-f0d2ea49e0f1`, `e828efa5-5c6d-43f3-9909-03f5ac232b98` | GHSA-38jv-5279-wg99, GHSA-2xpw-w6gg-jr37, GHSA-gm62-xv2j-4w53 | requirements.txt, uv.lock | -| Cargo | `pkg:cargo/traitobject@0.1.1` | `cf2e6f58-d9fa-4096-9151-c34afa717f89` | GHSA-pp8r-vv2j-9j5v | cargo sparse-registry leg | | RubyGems | `pkg:gem/activestorage@6.0.3` | any of `15e960b5-f432-4b6c-b8aa-534a2b419323` (GHSA-m42x-37p3-fv5w / CVE-2020-8162), `6c4141c5-1535-4fd2-9db1-b5f8e4834bdb` (GHSA-w749-p3v6-hccq / CVE-2022-21831, published 2026-08-19), `eeb6bf9f-96c0-4963-a0f1-2e88f91f8b1a` (GHSA-9xrj-h377-fr87 / CVE-2026-33195, published 2026-08-20), `c1a1cd3c-b670-4e44-b4fa-1a63ecd42db6` (GHSA-r4mg-4433-c7g3 / CVE-2025-24293, published 2026-08-20) | see UUID column | bundler leg | urllib3 1.26.18 carries **three** distinct free patches, one per advisory. Which @@ -48,7 +47,7 @@ one the resolver returns is a server-side ordering detail, so the suite accepts any of the three rather than pinning one — pinning would go red on an unrelated server-side reorder. -`preflight_required_patches_are_published` checks all four every run and fails +`preflight_required_patches_are_published` checks all three every run and fails first with the offending PURL named, so a withdrawn patch produces one clear failure instead of N confusing ones that look like CLI regressions. @@ -63,9 +62,9 @@ failure instead of N confusing ones that look like CLI regressions. package manager in that ecosystem's leg. 2. Update the catalog constants at the top of `e2e_hosted_production.rs` (`*_PURL`, `*_NAME`, `*_VERSION`, `*_UUID`) **and** the table above. -3. If the new patch does not inject the `// Socket Community Patch` header - (Cargo crates do not), pick a marker unique to the patch and set the - ecosystem's `*_MARKER` constant. +3. If the new patch does not inject the `// Socket Community Patch` header, + pick a marker unique to the patch and set the ecosystem's `*_MARKER` + constant. ## Ecosystem coverage, and the honest gaps @@ -73,23 +72,35 @@ failure instead of N confusing ones that look like CLI regressions. |-----------|-------------|----------------------------|----------------| | npm | ✅ | ✅ many | ✅ npm, npm-shrinkwrap, pnpm, yarn classic, yarn berry, bun | | PyPI | ✅ (requirements.txt + uv.lock only) | ✅ many | ✅ requirements.txt, uv.lock | -| Cargo | ✅ | ✅ 1 crate | ✅ sparse registry | | RubyGems | ✅ | ✅ (this suite pins one purl/UUID: `activestorage@6.0.3`; the 2026-08-18 republish covers more versions) | ✅ full bundler install proof | +| Cargo | ✅ | ❌ **none** (tier emptied 2026-08-28) | canary only | | Maven | ✅ | ❌ **none** | canary only | | NuGet | ✅ | ❌ **none** | canary only | | Composer | ✅ | ❌ **none** | canary only | | Go | ✅ free tier [by design](../design/golang-hosted.md) (paid: ❌ [analysis](../design/golang-hosted-no-go.md)) | ❌ none published yet | shape guard (redirects only via `goproxy` override) | | Deno | ❌ not supported | — | negative assertion | -Maven, NuGet and Composer all *implement* hosted mode, but production publishes -**zero** free-tier patches for them, so there is nothing real to redirect to. -Rather than skipping silently, `canary_unpublished_ecosystems` probes production -every run and reports the moment that changes, so coverage can be extended -deliberately. It does not fail when patches appear — production publishing a -patch is not a socket-patch regression — but +Cargo, Maven, NuGet and Composer all *implement* hosted mode, but production +publishes **zero** free-tier patches for them, so there is nothing real to +redirect to. Rather than skipping silently, `canary_unpublished_ecosystems` +probes production every run and reports the moment that changes, so coverage +can be extended deliberately. It does not fail when patches appear — production +publishing a patch is not a socket-patch regression — but `SOCKET_PATCH_HOSTED_E2E_CANARY_STRICT=1` makes it fail, for use in a scheduled nag run. +**Cargo was demoted to the canary on 2026-09-01.** Until then the suite carried +a full sparse-registry install proof pinned to one crate, but production's free +cargo tier emptied on 2026-08-28 (both pinned patches were deleted server-side, +leaving zero live free patches for any cargo crate), so there is nothing honest +left to pin. This deliberately drops the cargo install-proof coverage — the +canary only watches for the tier lighting up again. To re-promote cargo: pick a +live free patch and follow +[the withdrawn-patch procedure](#if-a-required-patch-is-withdrawn); the git +history of this demotion shows every piece to restore (catalog constants, +preflight registration, the install-proof leg, and these tables) in both +production suites. + PyPI's poetry / pdm / pipenv locks are **not** rewritten by hosted mode (see the [matrix](../ecosystems.md#mode--ecosystem-matrix)); those flavors are vendored-mode only, so there is no hosted leg to write for them. @@ -194,18 +205,18 @@ runs only where it is explicitly asked for. | Variable | Effect | |----------|--------| | `SOCKET_PATCH_HOSTED_E2E_STRICT=1` | Turn every "toolchain missing" soft-skip into a hard failure. **CI sets this** — a required check must never report green on an unexercised leg. | -| `SOCKET_PATCH_HOSTED_E2E_CANARY_STRICT=1` | Fail when maven/nuget/composer gain their first free published patch. | +| `SOCKET_PATCH_HOSTED_E2E_CANARY_STRICT=1` | Fail when cargo/maven/nuget/composer gain their first free published patch. | ### Toolchains -`npm`, `corepack` (pnpm + yarn classic + yarn berry), `bun`, `uv`, `cargo`, +`npm`, `corepack` (pnpm + yarn classic + yarn berry), `bun`, `uv`, `ruby` + `bundle` (**≥ 2.6** — `bundle lock --add-checksums` emits the CHECKSUMS section the gem rewrite pins into), `go`. ### Network egress `patches-api.socket.dev`, `patch.socket.dev`, `registry.npmjs.org`, `pypi.org`, -`files.pythonhosted.org`, `static.crates.io`, `index.crates.io`, `rubygems.org`. +`files.pythonhosted.org`, `rubygems.org`. ## CI: the `hosted-e2e` job diff --git a/docs/testing/vendored-production-e2e.md b/docs/testing/vendored-production-e2e.md index cfb2f7e8..04c7f95a 100644 --- a/docs/testing/vendored-production-e2e.md +++ b/docs/testing/vendored-production-e2e.md @@ -43,13 +43,12 @@ matches how the `e2e_vendor_*_build.rs` capstones assert. Pinned to these free-tier patches; they must stay published on `patches-api.socket.dev`. `preflight_required_patches_are_published` checks all -four every run and fails first with the offending PURL named. +three every run and fails first with the offending PURL named. | Ecosystem | PURL | Patch UUID | Marker in the patched bytes | |-----------|------|------------|-----------------------------| | npm | `pkg:npm/minimist@1.2.2` | `80630680-4da6-45f9-bba8-b888e0ffd58c` | `Socket Community Patch` header | | PyPI | `pkg:pypi/urllib3@1.26.18` | one of three (server-ordered) | `Socket Community Patch` header | -| Cargo | `pkg:cargo/traitobject@0.1.1` | `cf2e6f58-d9fa-4096-9151-c34afa717f89` | advisory id `GHSA-pp8r-vv2j-9j5v` | | RubyGems | `pkg:gem/activestorage@6.0.3` | any of the `GEM_PATCHES` table (4 as of 2026-08-20 — see the hosted doc's UUID list; each patch marks a different file) | `Socket Community Patch` header | If a required patch is withdrawn, update the catalog constants at the top of @@ -67,19 +66,22 @@ hosted suite). | bun (text lockfile) | minimist@1.2.2 | `bun install --frozen-lockfile` | ✅ full | | pip (requirements.txt) | urllib3@1.26.18 | `pip install --no-index -r requirements.txt` | ✅ full | | uv (uv.lock) | urllib3@1.26.18 | `uv sync --frozen --offline` | ✅ full | -| cargo (`[patch.crates-io]`) | traitobject@0.1.1 | `cargo fetch --offline --locked` (see note) | ✅ full | | bundler | activestorage@6.0.3 | frozen `bundle install`, fresh empty `BUNDLE_PATH` | ✅ full | | go | — | — | zero-patch assertion (no free golang patches) | | deno | — | — | negative assertion (unsupported) | -| maven / nuget / composer | — | — | canary (no free production patches) | - -The cargo delivery proof uses `cargo fetch --offline`, not `cargo build`, -because the production traitobject patch injects a `compile_error!` unless the -`allow-unmaintained` Cargo feature is enabled — the patch's whole point is to -make the unmaintained crate refuse to compile. That is patch *content*, not a -vendoring defect; the leg proves the artifact resolves entirely from the -committable files with zero registry downloads, and byte-checks the vendored -directory. +| cargo / maven / nuget / composer | — | — | canary (no free production patches) | + +Cargo sat in the ✅-full rows until 2026-09-01: the leg vendored a pinned crate +as a `[patch.crates-io]` path dep and proved delivery with +`cargo fetch --offline --locked` from an empty `CARGO_HOME`. Production's free +cargo tier emptied on 2026-08-28 (the pinned patches were deleted server-side, +leaving zero live free patches for any cargo crate), so the leg was demoted to +`canary_unpublished_vendored_ecosystems` — deliberately dropping the cargo +install-proof coverage. To re-promote: pick a live free cargo patch and follow +the withdrawn-patch procedure in the +[hosted doc](./hosted-production-e2e.md#if-a-required-patch-is-withdrawn); the +git history of this demotion shows every piece to restore in both production +suites. ## Known issues this suite surfaced @@ -170,7 +172,7 @@ installs from contending on the shared cache sandbox. | Variable | Effect | |----------|--------| | `SOCKET_PATCH_VENDORED_E2E_STRICT=1` | Turn every "toolchain missing" soft-skip into a hard failure. | -| `SOCKET_PATCH_VENDORED_E2E_CANARY_STRICT=1` | Fail when maven/nuget/composer gain their first free published patch. | +| `SOCKET_PATCH_VENDORED_E2E_CANARY_STRICT=1` | Fail when cargo/maven/nuget/composer gain their first free published patch. | The suite forces `SOCKET_NO_CONFIG=true` and scrubs every ambient `SOCKET_*` var, planting hostile seeds so a dropped scrub reddens the suite instead of @@ -180,10 +182,9 @@ token is used. ### Toolchains `npm`, `pnpm`, `corepack` (yarn classic + berry), `bun`, `uv`, `python3` (pip), -`cargo`, `ruby` + `bundle`, `go`. +`ruby` + `bundle`, `go`. ### Network egress `patches-api.socket.dev`, `patch.socket.dev`, `registry.npmjs.org`, `pypi.org`, -`files.pythonhosted.org`, `static.crates.io`, `index.crates.io`, -`rubygems.org`. +`files.pythonhosted.org`, `rubygems.org`. From 278eefefe1332717f118dee4457e6389e612d7ae Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Fri, 28 Aug 2026 11:43:07 -0400 Subject: [PATCH 2/3] fix(ci): regenerate the npm wrapper lock with CI's npm major MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit release-readiness regenerates the lock with the runner's npm 10, which does not write the `libc` platform arrays npm >= 11 emits — so the npm-11-shaped lock from #233 makes 'version-sync.sh 4.0.0 is not a no-op' fail on every PR and on main. The check's npm is the effective canon; note the npm-major dependence for whoever next refreshes the lock locally. Co-Authored-By: Claude Fable 5 --- npm/socket-patch/package-lock.json | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/npm/socket-patch/package-lock.json b/npm/socket-patch/package-lock.json index 987aca5f..0bd34b59 100644 --- a/npm/socket-patch/package-lock.json +++ b/npm/socket-patch/package-lock.json @@ -84,9 +84,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -100,9 +97,6 @@ "cpu": [ "arm" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -116,9 +110,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -132,9 +123,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -148,9 +136,6 @@ "cpu": [ "ia32" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -164,9 +149,6 @@ "cpu": [ "ia32" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -180,9 +162,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -196,9 +175,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ From 47f1c05b8e0e40e68ea8b6672427dcd287527b3b Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Mon, 31 Aug 2026 13:12:56 -0400 Subject: [PATCH 3/3] fix(ci): pin the npm wrapper lock refresh to npm@10 via npx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The release-readiness gate re-runs version-sync.sh and compares the regenerated npm/socket-patch/package-lock.json byte-for-byte, so the lock's canonical shape is defined by whatever npm regenerates it. npm 11 adds libc arrays that npm 10 omits, which is how the #233 refresh (made locally with npm 11) broke the gate under CI's npm 10 — and the same drift would recur in reverse the day the runner image jumps to npm 11. Pinning the refresh via npx makes the gate independent of both the runner default and the developer's local npm; bumping the pin now takes a deliberate commit that refreshes the lock alongside it. Co-Authored-By: Claude Fable 5 --- scripts/version-sync.sh | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/version-sync.sh b/scripts/version-sync.sh index 3451a9cb..48fde5f7 100755 --- a/scripts/version-sync.sh +++ b/scripts/version-sync.sh @@ -38,9 +38,17 @@ node -e " # Refresh the npm wrapper lockfile so package-lock.json stays in sync with the # bumped package.json (own version, optionalDependencies). Uses --package-lock-only # so node_modules is untouched. +# +# The npm major is pinned: the lock's byte shape depends on it (npm >= 11 adds +# `libc` arrays to platform-package entries that npm 10 omits), and +# release-lint.sh compares the regenerated lock byte-for-byte against the +# committed one on every PR. An unpinned npm makes that gate depend on +# whichever npm the runner or developer happens to have. Bumping this pin +# requires refreshing the committed lock in the same commit. +NPM_LOCK_REFRESH_VERSION="10" ( cd "$REPO_ROOT/npm/socket-patch" - npm install --package-lock-only --ignore-scripts >/dev/null + npx --yes "npm@$NPM_LOCK_REFRESH_VERSION" install --package-lock-only --ignore-scripts >/dev/null ) # Update all per-platform npm package versions