diff --git a/CHANGELOG.md b/CHANGELOG.md index ae097917..ca51ad24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -169,9 +169,9 @@ into the new version's section — see docs/releasing.md. silently shadows any repository). Both are proven by docker capstones against the real .NET SDK / Apache Maven (cold-cache, `--network none`, RED + TAMPER probes). `nuget` and `maven` are now DEFAULT compile features; - in-place agent apply for both remains runtime-gated - (`SOCKET_EXPERIMENTAL_NUGET=1` / `SOCKET_EXPERIMENTAL_MAVEN=1` — sidecar - corruption risk) while the committable vendor path is safe. The vendored + the `SOCKET_EXPERIMENTAL_NUGET` / `SOCKET_EXPERIMENTAL_MAVEN` runtime + opt-ins that briefly gated in-place agent apply were retired later in this + cycle (see the "promoted to fully available" entry under Changed). The vendored path convention + uuid recovery rule now covers `nuget` and `maven` dirs, and `--vendor-source` prebuilt downloads cover nuget. - **Maven hosted rewriter (pom projects) — fail-closed version suffixing + @@ -337,6 +337,24 @@ into the new version's section — see docs/releasing.md. ### Changed +- **Maven and NuGet promoted to fully available — the + `SOCKET_EXPERIMENTAL_MAVEN` / `SOCKET_EXPERIMENTAL_NUGET` runtime gates + are retired.** Every flow (`scan` in all modes, `apply`, `get`, + `rollback`, `vendor`, `repair`, `vex`, `setup`) now discovers and + patches installed Maven and NuGet packages unconditionally; the + "N patch(es) skipped — support is experimental" warnings are gone, and + the previously `#[ignore]`d maven/nuget dispatch e2e tests now gate CI. + Setting the old env vars is harmless but does nothing. Behavior notes: + a default `scan` now walks the local Maven repository (`~/.m2` / + `MAVEN_REPO_LOCAL`) and the NuGet caches, and `scan --prune`/`--sync` + now judges maven/nuget manifest entries like any other ecosystem's + (previously they were exempt from pruning while the gate was closed). + The in-place sidecar caveat is unchanged and now documented per mode in + `docs/ecosystems.md`: agent-mode patching leaves Maven's + `.jar.sha1`/`.jar.md5` stale and NuGet's fixup deletes + `.nupkg.metadata` + advises on `.nupkg.sha512`; the vendored/hosted + modes never touch the caches. + - **Release workflow consolidated into a single `release.yml`.** One dispatch now publishes every ecosystem package — crates.io, npm, PyPI, RubyGems (both gems, via OIDC trusted publishing), Packagist, Maven @@ -361,9 +379,10 @@ into the new version's section — see docs/releasing.md. Maven, Composer, and Deno support is now unconditional. Builds that passed `--features ` will get an "unknown feature" error and should simply drop the flag; `--no-default-features` no longer produces a minimal binary - (there is nothing left to strip). The runtime gates are unchanged: - Maven/NuGet crawling and apply still require `SOCKET_EXPERIMENTAL_MAVEN=1` / - `SOCKET_EXPERIMENTAL_NUGET=1`. The only remaining features are the + (there is nothing left to strip). The `SOCKET_EXPERIMENTAL_MAVEN` / + `SOCKET_EXPERIMENTAL_NUGET` runtime gates outlived this entry only briefly — + they are retired in the same release (see the "promoted to fully available" + entry under Changed). The only remaining features are the test-suite gates `docker-e2e` and `setup-e2e` on `socket-patch-cli`. (MAJOR for anyone scripting `--features`; no behavior change for default builds beyond composer/deno support now being present.) diff --git a/README.md b/README.md index a243fce6..2e265426 100644 --- a/README.md +++ b/README.md @@ -129,10 +129,7 @@ the edit. > If it prints `No patches available for installed packages.`, none of your installed > dependency versions currently has a Socket patch — the good outcome, with nothing to -> apply. (One exception: Maven and NuGet installed-package discovery is experimental and -> off by default — `scan` silently skips them unless `SOCKET_EXPERIMENTAL_MAVEN=1` / -> `SOCKET_EXPERIMENTAL_NUGET=1` is set; see the -> [mode × ecosystem matrix](docs/ecosystems.md#mode--ecosystem-matrix).) +> apply. > To walk the rest of the loop anyway, make a scratch project pinned to a version > that has a free patch — at the time of writing, `flatted@3.3.1`: > @@ -727,10 +724,12 @@ What gets wired, per ecosystem: `"setup": { "manual": ["deno"] }`) so [`vex`](#vex) still attests the hand-applied patches — this matters most for Deno, which has no vendored or hosted alternative. For Maven - and NuGet, discovery of installed packages is experimental and off by default (opt in - with `SOCKET_EXPERIMENTAL_MAVEN=1` / `SOCKET_EXPERIMENTAL_NUGET=1`), and in-place - patching corrupts their cache checksum sidecars — prefer `--mode vendored` or - `--mode hosted`; see [ecosystems.md](docs/ecosystems.md#maven--nuget-caveats). + and NuGet, note that in-place patching leaves the caches' own checksum sidecars stale + (NuGet's fixup deletes `.nupkg.metadata` and raises an advisory for the signed-package + `.nupkg.sha512` marker; Maven's `.jar.sha1`/`.jar.md5` are left as-is) — the copy-out + modes, `scan --mode vendored` and `scan --mode hosted`, never touch the caches and + avoid the issue entirely. See + [ecosystems.md](docs/ecosystems.md#maven--nuget-caveats). **Usage:** ```bash diff --git a/crates/socket-patch-cli/CLI_CONTRACT.md b/crates/socket-patch-cli/CLI_CONTRACT.md index f74b21f3..f4c5d7ea 100644 --- a/crates/socket-patch-cli/CLI_CONTRACT.md +++ b/crates/socket-patch-cli/CLI_CONTRACT.md @@ -746,8 +746,6 @@ These exist for staged rollouts and the launcher wrappers. They are **internal** | Env var | Purpose | |---|---| -| `SOCKET_EXPERIMENTAL_MAVEN` | Opt-in gate (`=1`) for the maven installed-package crawl behind `scan`/`apply`/`vendor` — agent-mode in-place jar patching corrupts the `~/.m2` checksum sidecars, so discovery stays off by default (`src/ecosystem_dispatch.rs`). | -| `SOCKET_EXPERIMENTAL_NUGET` | Same gate for nuget — in-place patching breaks the `.nupkg.sha512` tamper-evidence sidecar. | | `SOCKET_PATCH_BIN` | Points the CLI launcher wrappers (RubyGems / Composer / Maven / NuGet) and the gem Bundler plugin at an existing `socket-patch` binary (skips the download-on-first-run); also the escape hatch `apply` names when a golang-featureless binary is asked to audit Go redirects. | | `SOCKET_UPDATE_BASE_URL` | Points BOTH the release-metadata and asset-download routes of `--update`/the update notice at one base (mirror or test fixture) instead of `github.com` + `api.github.com`. Overriding it relaxes the downloaded binary's version self-check from hard-fail to warning. | | `SOCKET_UPDATE_STATE_DIR` | Overrides the per-user dir holding `update-check.json` + `update.lock` (tests point it into a tempdir). | diff --git a/crates/socket-patch-cli/Cargo.toml b/crates/socket-patch-cli/Cargo.toml index 16d3651b..06abdd21 100644 --- a/crates/socket-patch-cli/Cargo.toml +++ b/crates/socket-patch-cli/Cargo.toml @@ -35,9 +35,9 @@ libc = { workspace = true } [features] # Every ecosystem (npm, PyPI, Ruby gems, Go, Cargo, NuGet, Maven, Composer, -# Deno) is unconditionally compiled in — there are no ecosystem feature gates. -# Maven `apply` stays runtime-gated behind `SOCKET_EXPERIMENTAL_MAVEN=1` -# (in-place jar patching corrupts sidecars); committable `vendor` is safe. +# Deno) is unconditionally compiled in AND enabled at runtime — there are no +# ecosystem feature gates and no runtime env gates (the old +# SOCKET_EXPERIMENTAL_MAVEN/NUGET opt-ins are retired). # The only features left gate opt-in test suites: # # Enables the Docker-driven real-package e2e test suite under diff --git a/crates/socket-patch-cli/src/commands/scan/gc.rs b/crates/socket-patch-cli/src/commands/scan/gc.rs index 094e2d6e..4eaf7278 100644 --- a/crates/socket-patch-cli/src/commands/scan/gc.rs +++ b/crates/socket-patch-cli/src/commands/scan/gc.rs @@ -273,13 +273,13 @@ pub(super) fn print_gc_vendored_line(gc: &GcSummary) { /// `--sync` would GC the very patch it just downloaded. /// /// Entries the crawl never even looked for are exempt too -/// (`crawl_covers_purl`): the runtime-gated maven/nuget crawlers with their -/// gate off, and any `pkg:/` this build has no crawler for. The -/// manifest is a committed, shared file, so a newer CLI's ecosystem can -/// legitimately appear in it — "absent from the crawl" then says nothing -/// about whether the package is installed, and pruning would silently -/// delete a teammate's patch (plus its blobs). Same fail-safe reasoning as -/// capturing `scanned_purls` before the `--ecosystems` filter. +/// (`crawl_covers_purl`): any `pkg:/` this build has no crawler +/// for. The manifest is a committed, shared file, so a newer CLI's +/// ecosystem can legitimately appear in it — "absent from the crawl" +/// then says nothing about whether the package is installed, and pruning +/// would silently delete a teammate's patch (plus its blobs). Same +/// fail-safe reasoning as capturing `scanned_purls` before the +/// `--ecosystems` filter. fn detect_prunable( manifest: &PatchManifest, scanned_purls: &HashSet, @@ -455,40 +455,27 @@ mod tests { } #[test] - fn detect_prunable_keeps_runtime_gated_ecosystem_entries() { - // Maven/NuGet crawlers only run under their experimental opt-in - // env gates, so with the gate OFF their packages are invisible to - // the crawl — "absent" must not mean "uninstalled". (An ambient - // opt-in makes them genuinely crawled, which is a different - // scenario; skip rather than mutate the shared process env.) + fn detect_prunable_judges_maven_and_nuget_like_any_ecosystem() { + // Maven/NuGet are first-class ecosystems: every scan crawls them, + // so their manifest entries ARE judged — absent from the scan + // means genuinely uninstalled, and they prune like any other + // ecosystem. (They used to be exempt behind the retired + // `SOCKET_EXPERIMENTAL_*` runtime gates.) let m = manifest_with(&[ ("pkg:maven/com.example/lib@1.0.0", "uuid-a"), ("pkg:nuget/Some.Package@1.0.0", "uuid-b"), ("pkg:npm/gone@1.0.0", "uuid-c"), ]); - // Mirror `ecosystem_dispatch::env_truthy` exactly — the gate opens - // only on `1`/`true` (any case), so a merely-PRESENT but falsy - // `SOCKET_EXPERIMENTAL_MAVEN=0` leaves the ecosystem uncrawled and - // therefore exempt. Testing presence instead of truthiness made this - // expectation disagree with the code under test and fail spuriously. - let gate_open = |name: &str| { - std::env::var(name) - .map(|v| v == "1" || v.eq_ignore_ascii_case("true")) - .unwrap_or(false) - }; - let mut expected = vec!["pkg:npm/gone@1.0.0".to_string()]; - if gate_open("SOCKET_EXPERIMENTAL_MAVEN") { - expected.push("pkg:maven/com.example/lib@1.0.0".to_string()); - } - if gate_open("SOCKET_EXPERIMENTAL_NUGET") { - expected.push("pkg:nuget/Some.Package@1.0.0".to_string()); - } - expected.sort(); let mut out = detect_prunable(&m, &scanned(&[]), &no_vendored()); out.sort(); assert_eq!( - out, expected, - "a runtime-gated ecosystem that was never crawled must not prune" + out, + vec![ + "pkg:maven/com.example/lib@1.0.0".to_string(), + "pkg:npm/gone@1.0.0".to_string(), + "pkg:nuget/Some.Package@1.0.0".to_string(), + ], + "maven/nuget orphans must prune like any other ecosystem's" ); } diff --git a/crates/socket-patch-cli/src/ecosystem_dispatch.rs b/crates/socket-patch-cli/src/ecosystem_dispatch.rs index d2b44ea4..08c961bd 100644 --- a/crates/socket-patch-cli/src/ecosystem_dispatch.rs +++ b/crates/socket-patch-cli/src/ecosystem_dispatch.rs @@ -14,67 +14,14 @@ use socket_patch_core::crawlers::GoCrawler; use socket_patch_core::crawlers::MavenCrawler; use socket_patch_core::crawlers::NuGetCrawler; -/// Runtime opt-in gate for experimental Maven support. -/// -/// The Maven crawler does NOT run unless `SOCKET_EXPERIMENTAL_MAVEN=1` (or -/// `=true`). Applying a Maven patch corrupts the jar sidecar -/// checksums (`.jar.sha1`, `.jar.md5`) that the local -/// Maven repository keeps next to each artifact, and there is no -/// recovery — the user has to re-download the jar. -fn maven_runtime_enabled() -> bool { - env_truthy("SOCKET_EXPERIMENTAL_MAVEN") -} - -fn warn_maven_disabled(skipped: usize) { - eprintln!( - "Warning: {} Maven patch(es) skipped — Maven support is experimental.", - skipped - ); - eprintln!(" Maven patches corrupt jar sidecar checksums (sha1/md5)."); - eprintln!(" Set SOCKET_EXPERIMENTAL_MAVEN=1 to enable at your own risk."); -} - -/// Runtime opt-in gate for experimental NuGet support. Same shape as -/// the Maven gate. Even with the sidecar fixup deleting -/// `.nupkg.metadata`, signed packages still carry a `.nupkg.sha512` -/// marker that NuGet treats as tamper-evidence at restore time. The -/// fixup cannot honestly rewrite this without the original `.nupkg` -/// (which we don't have post-extraction). Refuse to dispatch unless -/// the operator has explicitly opted in to the experimental tier. -fn nuget_runtime_enabled() -> bool { - env_truthy("SOCKET_EXPERIMENTAL_NUGET") -} - -fn warn_nuget_disabled(skipped: usize) { - eprintln!( - "Warning: {} NuGet patch(es) skipped — NuGet support is experimental.", - skipped - ); - eprintln!(" NuGet patches corrupt the .nupkg.sha512 signature sidecar that"); - eprintln!(" `dotnet restore` reads as tamper-evidence."); - eprintln!(" Set SOCKET_EXPERIMENTAL_NUGET=1 to enable at your own risk."); -} - -fn env_truthy(name: &str) -> bool { - std::env::var(name) - .map(|v| v == "1" || v.eq_ignore_ascii_case("true")) - .unwrap_or(false) -} - /// Whether [`crawl_all_ecosystems`] actually visits this PURL's ecosystem -/// in THIS process. Maven and NuGet sit behind runtime opt-in gates, and an -/// unrecognized `pkg:/` (a newer CLI's ecosystem in a committed -/// manifest) has no crawler at all — for those, absence from the crawl -/// carries no information about whether the package is installed. Callers -/// that read "not in the crawl" as "no longer installed" (scan's prune GC) -/// must not judge them. +/// in THIS process. An unrecognized `pkg:/` (a newer CLI's ecosystem +/// in a committed manifest) has no crawler at all — for those, absence +/// from the crawl carries no information about whether the package is +/// installed. Callers that read "not in the crawl" as "no longer +/// installed" (scan's prune GC) must not judge them. pub fn crawl_covers_purl(purl: &str) -> bool { - match Ecosystem::from_purl(purl) { - Some(Ecosystem::Maven) => maven_runtime_enabled(), - Some(Ecosystem::Nuget) => nuget_runtime_enabled(), - Some(_) => true, - None => false, - } + Ecosystem::from_purl(purl).is_some() } /// Partition PURLs by ecosystem, filtering by the `--ecosystems` flag if set. @@ -301,31 +248,23 @@ async fn dispatch_find( on_match = merge_first_wins, ); - if let Some(maven_purls) = partitioned.get(&Ecosystem::Maven) { - if !maven_purls.is_empty() && !maven_runtime_enabled() { - if !silent { - warn_maven_disabled(maven_purls.len()); - } - } else { - scan_ecosystem!( - out = out, - partitioned = partitioned, - eco = Ecosystem::Maven, - options = options, - silent = silent, - crawler = MavenCrawler, - get_paths = get_maven_repo_paths, - using_label = "Maven repository", - err_label = "Maven packages", - // Maven has per-classifier release variants - // (`?classifier=&ext=`) that coexist as distinct jars in - // one version dir; the crawler emits the base PURL and - // each variant is resolved by hashing its jar file. - purls_override = dedup_qualified_purls, - on_match = variant_merge, - ); - } - } + scan_ecosystem!( + out = out, + partitioned = partitioned, + eco = Ecosystem::Maven, + options = options, + silent = silent, + crawler = MavenCrawler, + get_paths = get_maven_repo_paths, + using_label = "Maven repository", + err_label = "Maven packages", + // Maven has per-classifier release variants + // (`?classifier=&ext=`) that coexist as distinct jars in + // one version dir; the crawler emits the base PURL and + // each variant is resolved by hashing its jar file. + purls_override = dedup_qualified_purls, + on_match = variant_merge, + ); scan_ecosystem!( out = out, @@ -341,27 +280,19 @@ async fn dispatch_find( on_match = merge_first_wins, ); - if let Some(nuget_purls) = partitioned.get(&Ecosystem::Nuget) { - if !nuget_purls.is_empty() && !nuget_runtime_enabled() { - if !silent { - warn_nuget_disabled(nuget_purls.len()); - } - } else { - scan_ecosystem!( - out = out, - partitioned = partitioned, - eco = Ecosystem::Nuget, - options = options, - silent = silent, - crawler = NuGetCrawler, - get_paths = get_nuget_package_paths, - using_label = "NuGet packages", - err_label = "NuGet packages", - purls_override = passthrough_purls, - on_match = merge_first_wins, - ); - } - } + scan_ecosystem!( + out = out, + partitioned = partitioned, + eco = Ecosystem::Nuget, + options = options, + silent = silent, + crawler = NuGetCrawler, + get_paths = get_nuget_package_paths, + using_label = "NuGet packages", + err_label = "NuGet packages", + purls_override = passthrough_purls, + on_match = merge_first_wins, + ); scan_ecosystem!( out = out, @@ -429,7 +360,7 @@ pub async fn find_manifest_package_paths( find_packages_for_rollback(&partitioned, &crawler_options, quiet).await } -/// Crawl all enabled ecosystems and return all packages plus per-ecosystem counts. +/// Crawl all ecosystems and return all packages plus per-ecosystem counts. pub async fn crawl_all_ecosystems( options: &CrawlerOptions, ) -> (Vec, HashMap) { @@ -449,16 +380,9 @@ pub async fn crawl_all_ecosystems( crawl!(Ecosystem::Cargo, CargoCrawler); crawl!(Ecosystem::Gem, RubyCrawler); crawl!(Ecosystem::Golang, GoCrawler); - if maven_runtime_enabled() { - // Same runtime gate as `find_packages_for_purls` — `scan` - // walks the Maven repo only when the operator has explicitly - // opted into experimental support. - crawl!(Ecosystem::Maven, MavenCrawler); - } + crawl!(Ecosystem::Maven, MavenCrawler); crawl!(Ecosystem::Composer, ComposerCrawler); - if nuget_runtime_enabled() { - crawl!(Ecosystem::Nuget, NuGetCrawler); - } + crawl!(Ecosystem::Nuget, NuGetCrawler); crawl!(Ecosystem::Deno, DenoCrawler); (all_packages, counts) @@ -718,36 +642,6 @@ mod tests { assert!(!Ecosystem::Deno.supports_release_variants()); } - #[test] - fn env_truthy_accepts_one_and_true_case_insensitive() { - let key = "SOCKET_TEST_ENV_TRUTHY"; - std::env::set_var(key, "1"); - assert!(env_truthy(key)); - std::env::set_var(key, "TrUe"); - assert!(env_truthy(key)); - std::env::set_var(key, "0"); - assert!(!env_truthy(key)); - std::env::set_var(key, "yes"); - assert!(!env_truthy(key)); - std::env::remove_var(key); - assert!(!env_truthy(key)); - } - - #[test] - fn env_truthy_rejects_empty_and_padded_values() { - // The experimental gates must NOT open on an empty assignment - // (`SOCKET_EXPERIMENTAL_MAVEN=`) or on whitespace-padded values — - // only the exact tokens `1` / `true` (any case) enable them. - let key = "SOCKET_TEST_ENV_TRUTHY_EDGE"; - for falsey in ["", " ", "1 ", " 1", "1\n", "true ", "tru", "11", "01"] { - std::env::set_var(key, falsey); - assert!(!env_truthy(key), "{falsey:?} must not be truthy"); - } - std::env::set_var(key, "TRUE"); - assert!(env_truthy(key)); - std::env::remove_var(key); - } - #[test] fn partition_purls_no_filter_single_npm() { let purls = vec!["pkg:npm/foo@1.0".to_string()]; @@ -977,68 +871,72 @@ mod tests { .is_empty()); } - // ---- experimental Maven/NuGet runtime gates -------------------------- + // ---- Maven/NuGet are first-class ecosystems --------------------------- // - // `crawl_all_ecosystems` only walks Maven / NuGet when the operator has - // opted in via `SOCKET_EXPERIMENTAL_*`. The gate's observable effect is - // whether the ecosystem appears in the returned per-ecosystem `counts` - // map at all: a crawled-but-empty ecosystem gets a `0` entry; a gated-off - // one gets no entry. That distinction lets us test the gate without a - // real Maven repo / NuGet cache fixture. - + // Maven and NuGet used to sit behind `SOCKET_EXPERIMENTAL_MAVEN` / + // `SOCKET_EXPERIMENTAL_NUGET` runtime gates. The gates are gone: every + // ecosystem is crawled unconditionally in every flow. The observable + // pin is the per-ecosystem `counts` map — a crawled-but-empty ecosystem + // gets a `0` entry, so presence proves the crawler ran without needing + // a real Maven repo / NuGet cache fixture. + + /// Every ecosystem must appear in `counts` unconditionally — guards + /// against one being accidentally moved behind a runtime gate (the + /// regression this test replaces: maven/nuget were env-gated). #[tokio::test] - #[serial_test::serial(experimental_gate_env)] - async fn crawl_all_gates_maven_on_runtime_flag() { + async fn crawl_all_includes_every_ecosystem_unconditionally() { let tmp = tempfile::tempdir().unwrap(); - let opts = local_options(tmp.path().to_path_buf()); - - std::env::remove_var("SOCKET_EXPERIMENTAL_MAVEN"); - let (_, counts) = crawl_all_ecosystems(&opts).await; - assert!( - !counts.contains_key(&Ecosystem::Maven), - "Maven must not be crawled when the experimental flag is unset" - ); - - std::env::set_var("SOCKET_EXPERIMENTAL_MAVEN", "1"); - let (_, counts) = crawl_all_ecosystems(&opts).await; - assert!( - counts.contains_key(&Ecosystem::Maven), - "Maven must be crawled once the experimental flag is set" - ); - std::env::remove_var("SOCKET_EXPERIMENTAL_MAVEN"); + let (_, counts) = crawl_all_ecosystems(&local_options(tmp.path().to_path_buf())).await; + for eco in [ + Ecosystem::Npm, + Ecosystem::Pypi, + Ecosystem::Cargo, + Ecosystem::Gem, + Ecosystem::Golang, + Ecosystem::Maven, + Ecosystem::Composer, + Ecosystem::Nuget, + Ecosystem::Deno, + ] { + assert!( + counts.contains_key(&eco), + "{eco:?} must be crawled unconditionally — no runtime gates" + ); + } } + /// The PURL-lookup path (`find_packages_for_purls` — apply/vendor's + /// resolver) must resolve a maven package from a local repository with + /// no env opt-in of any kind. #[tokio::test] - #[serial_test::serial(experimental_gate_env)] - async fn crawl_all_gates_nuget_on_runtime_flag() { + #[serial_test::serial(maven_repo_env)] + async fn find_packages_resolves_maven_without_any_opt_in() { let tmp = tempfile::tempdir().unwrap(); - let opts = local_options(tmp.path().to_path_buf()); - std::env::remove_var("SOCKET_EXPERIMENTAL_NUGET"); - let (_, counts) = crawl_all_ecosystems(&opts).await; - assert!( - !counts.contains_key(&Ecosystem::Nuget), - "NuGet must not be crawled when the experimental flag is unset" - ); + // Minimal local Maven repository layout the crawler recognizes: + // /org/example/foo/1.0.0/foo-1.0.0.pom (+ project marker). + std::fs::write(tmp.path().join("pom.xml"), "\n").unwrap(); + let artifact_dir = tmp + .path() + .join("m2repo") + .join("org") + .join("example") + .join("foo") + .join("1.0.0"); + std::fs::create_dir_all(&artifact_dir).unwrap(); + std::fs::write(artifact_dir.join("foo-1.0.0.pom"), "").unwrap(); + std::env::set_var("MAVEN_REPO_LOCAL", tmp.path().join("m2repo")); + + let purl = "pkg:maven/org.example/foo@1.0.0".to_string(); + let partitioned = partition_purls(std::slice::from_ref(&purl), None); + let opts = local_options(tmp.path().to_path_buf()); - std::env::set_var("SOCKET_EXPERIMENTAL_NUGET", "1"); - let (_, counts) = crawl_all_ecosystems(&opts).await; - assert!( - counts.contains_key(&Ecosystem::Nuget), - "NuGet must be crawled once the experimental flag is set" + let out = find_packages_for_purls(&partitioned, &opts, true).await; + std::env::remove_var("MAVEN_REPO_LOCAL"); + assert_eq!( + out.get(&purl), + Some(&artifact_dir), + "maven lookup must resolve without any experimental opt-in" ); - std::env::remove_var("SOCKET_EXPERIMENTAL_NUGET"); - } - - /// The always-on ecosystems must appear in `counts` unconditionally — - /// guards against one being accidentally moved behind a runtime gate. - #[tokio::test] - #[serial_test::serial(experimental_gate_env)] - async fn crawl_all_always_includes_core_ecosystems() { - let tmp = tempfile::tempdir().unwrap(); - let (_, counts) = crawl_all_ecosystems(&local_options(tmp.path().to_path_buf())).await; - assert!(counts.contains_key(&Ecosystem::Npm)); - assert!(counts.contains_key(&Ecosystem::Pypi)); - assert!(counts.contains_key(&Ecosystem::Gem)); } } diff --git a/crates/socket-patch-cli/tests/common/mod.rs b/crates/socket-patch-cli/tests/common/mod.rs index 351f6f03..fb5a8a3e 100644 --- a/crates/socket-patch-cli/tests/common/mod.rs +++ b/crates/socket-patch-cli/tests/common/mod.rs @@ -60,8 +60,7 @@ pub fn run(cwd: &Path, args: &[&str]) -> (i32, String, String) { } /// `run` + child-only env-var injection. Useful for tests that need -/// to flip the per-ecosystem runtime gates (`SOCKET_EXPERIMENTAL_NUGET`) -/// or override discovery roots (`NUGET_PACKAGES`, `GOMODCACHE`) without +/// to override discovery roots (`NUGET_PACKAGES`, `GOMODCACHE`) without /// touching the parent process's environment — keeps tests parallel-safe. pub fn run_with_env(cwd: &Path, args: &[&str], env: &[(&str, &str)]) -> (i32, String, String) { run_bin_with_env(&binary(), cwd, args, env) diff --git a/crates/socket-patch-cli/tests/docker_e2e_maven.rs b/crates/socket-patch-cli/tests/docker_e2e_maven.rs index d88a15dd..7baf52cc 100644 --- a/crates/socket-patch-cli/tests/docker_e2e_maven.rs +++ b/crates/socket-patch-cli/tests/docker_e2e_maven.rs @@ -303,9 +303,8 @@ echo "===PATCH VERIFIED===" >&2 # Agent-mode VEX leg. The manifest scan --sync wrote carries {GHSA} (served in # the patch view); vex verifies the patched .pom on disk and attests it with # PLAIN agent provenance. --global/--ecosystems maven mirror the apply (the -# maven crawler probes ~/.m2, gated by SOCKET_EXPERIMENTAL_MAVEN=1 from the -# docker run env); --offline keeps vex local. The doc is emitted between -# markers for the host-side oracle (no bind mount here). +# maven crawler probes ~/.m2); --offline keeps vex local. The doc is emitted +# between markers for the host-side oracle (no bind mount here). echo "===VEX OUTPUT===" >&2 socket-patch vex --offline --cwd "$PWD" --output /tmp/out.vex.json \ --product 'pkg:maven/org.test/e2e@1.0.0' --global --ecosystems maven >/tmp/vex.out 2>/tmp/vex.err @@ -408,14 +407,6 @@ async fn maven_install_full_apply_chain() { "--rm", "--add-host=host.docker.internal:host-gateway", "-i", - // Maven crawler is gated by `SOCKET_EXPERIMENTAL_MAVEN=1` at - // runtime (see ecosystem_dispatch::maven_runtime_enabled). - // The gate exists because Maven apply corrupts jar sidecar - // checksums — operators have to opt in. Tests opt in - // explicitly so the docker run actually exercises the - // maven scan / apply path. - "-e", - "SOCKET_EXPERIMENTAL_MAVEN=1", ]) .args(cov_docker_args()) .args([ diff --git a/crates/socket-patch-cli/tests/docker_e2e_nuget.rs b/crates/socket-patch-cli/tests/docker_e2e_nuget.rs index 9e5381ed..155a44e3 100644 --- a/crates/socket-patch-cli/tests/docker_e2e_nuget.rs +++ b/crates/socket-patch-cli/tests/docker_e2e_nuget.rs @@ -301,9 +301,8 @@ echo "===PATCH VERIFIED===" >&2 # the patch view); vex verifies the patched LICENSE.md in the NUGET_PACKAGES # tree and attests it with PLAIN agent provenance. --ecosystems nuget (no # --global, matching the local apply; the crawler honors NUGET_PACKAGES exported -# above and is gated by SOCKET_EXPERIMENTAL_NUGET=1 from the docker run env); -# --offline keeps vex local. The doc is emitted between markers for the host -# oracle (no bind mount here). +# above); --offline keeps vex local. The doc is emitted between markers for the +# host oracle (no bind mount here). echo "===VEX OUTPUT===" >&2 socket-patch vex --offline --cwd "$PWD" --output /tmp/out.vex.json \ --product 'pkg:nuget/e2e-app@1.0.0' --ecosystems nuget >/tmp/vex.out 2>/tmp/vex.err @@ -527,15 +526,6 @@ fn run_container(script: &str) -> std::process::Output { "--rm", "--add-host=host.docker.internal:host-gateway", "-i", - // NuGet crawler is gated by `SOCKET_EXPERIMENTAL_NUGET=1` at - // runtime (see ecosystem_dispatch::nuget_runtime_enabled). - // Signed .nupkg packages carry a `.nupkg.sha512` tamper-marker - // the sidecar can't honestly rewrite without the original - // `.nupkg` bytes; the gate makes operators opt in to that - // tradeoff. Tests opt in explicitly so docker actually - // exercises the nuget scan / apply path. - "-e", - "SOCKET_EXPERIMENTAL_NUGET=1", ]) .args(cov_docker_args()) .args(["socket-patch-test-nuget:latest", "bash", "-c", script]); diff --git a/crates/socket-patch-cli/tests/docker_e2e_vendor_maven.rs b/crates/socket-patch-cli/tests/docker_e2e_vendor_maven.rs index be872a93..9b5db2b1 100644 --- a/crates/socket-patch-cli/tests/docker_e2e_vendor_maven.rs +++ b/crates/socket-patch-cli/tests/docker_e2e_vendor_maven.rs @@ -13,8 +13,8 @@ //! (`$M2`, bind-mounted) with commons-text + commons-lang3 + the plugin //! machinery → a marker patch on the extracted-jar's `META-INF/NOTICE.txt` //! is hand-staged (manifest + blob; git-blob sha256 from the ACTUAL cached -//! bytes) → `socket-patch vendor --json --offline` (baked binary, -//! `SOCKET_EXPERIMENTAL_MAVEN=1`) → asserts: the rebuilt `.jar` under the +//! bytes) → `socket-patch vendor --json --offline` (baked binary) → +//! asserts: the rebuilt `.jar` under the //! maven2 leaf `.socket/vendor/maven//…`, the verbatim upstream pom //! beside it (carrying the commons-lang3 transitive), the `.sha1` sidecars, //! `socket-patch.vendor.json`, `state.json`, the `` inserted @@ -93,10 +93,8 @@ const STAGE1: &str = r#" # point at it so warming, vendoring, and consumption all agree on one cache. export M2=/workspace/m2 export MAVEN_REPO_LOCAL="$M2" -# Keep socket-patch fully offline (also gates telemetry) + opt into the -# experimental Maven dispatch tier (the crawler is runtime-gated). +# Keep socket-patch fully offline (also gates telemetry). export SOCKET_OFFLINE=1 -export SOCKET_EXPERIMENTAL_MAVEN=1 MVN="mvn -q -Dmaven.repo.local=$M2 -Dmaven.test.skip=true -Dstyle.color=never" mkdir -p /workspace/proj && cd /workspace/proj @@ -288,7 +286,6 @@ const STAGE3: &str = r#" export M2=/workspace/m2 export MAVEN_REPO_LOCAL="$M2" export SOCKET_OFFLINE=1 -export SOCKET_EXPERIMENTAL_MAVEN=1 MVN="mvn -q -Dmaven.repo.local=$M2 -Dmaven.test.skip=true -Dstyle.color=never" cd /workspace/proj LEAF=".socket/vendor/maven/__UUID__/org/apache/commons/commons-text/1.10.0" diff --git a/crates/socket-patch-cli/tests/docker_e2e_vendor_nuget.rs b/crates/socket-patch-cli/tests/docker_e2e_vendor_nuget.rs index 9bcf98f4..c0c46730 100644 --- a/crates/socket-patch-cli/tests/docker_e2e_vendor_nuget.rs +++ b/crates/socket-patch-cli/tests/docker_e2e_vendor_nuget.rs @@ -9,8 +9,8 @@ //! `dotnet restore` resolves it from nuget.org and writes //! `packages.lock.json` → a marker patch on the extracted `LICENSE.md` is //! hand-staged (manifest + blob; git-blob sha256 from the ACTUAL installed -//! bytes) → `socket-patch vendor --json --offline` (the baked binary, with -//! `SOCKET_EXPERIMENTAL_NUGET=1`) → asserts: the rebuilt `.nupkg` under +//! bytes) → `socket-patch vendor --json --offline` (the baked binary) → +//! asserts: the rebuilt `.nupkg` under //! `.socket/vendor/nuget//`, `socket-patch.vendor.json`, `state.json`, //! the created `nuget.config` (our source + a `packageSourceMapping` for //! the id), and `packages.lock.json` repinned to `base64(sha512(nupkg))`; @@ -66,10 +66,8 @@ fn render(stage_body: &str) -> String { /// staging of ONLY the committable files. const STAGE1: &str = r#" mkdir -p /workspace/proj && cd /workspace/proj -# Keep the in-container socket-patch fully offline (also gates telemetry) and -# opt in to the experimental NuGet dispatch tier. +# Keep the in-container socket-patch fully offline (also gates telemetry). export SOCKET_OFFLINE=1 -export SOCKET_EXPERIMENTAL_NUGET=1 # Project-local global package cache so the crawler + rebuild find the nupkg # deterministically; stage 2 uses a DIFFERENT cold dir. export NUGET_PACKAGES="$PWD/.nuget-packages" @@ -230,7 +228,6 @@ exit 0 const STAGE3: &str = r#" cd /workspace/proj export SOCKET_OFFLINE=1 -export SOCKET_EXPERIMENTAL_NUGET=1 export NUGET_PACKAGES="$PWD/.nuget-packages" NUPKG=".socket/vendor/nuget/__UUID__/newtonsoft.json.13.0.3.nupkg" diff --git a/crates/socket-patch-cli/tests/e2e_maven.rs b/crates/socket-patch-cli/tests/e2e_maven.rs index 16d95dfa..d8714f7d 100644 --- a/crates/socket-patch-cli/tests/e2e_maven.rs +++ b/crates/socket-patch-cli/tests/e2e_maven.rs @@ -64,13 +64,6 @@ async fn run(args: &[&str], cwd: &Path, m2_repo: &Path, proxy_url: &str) -> Outp .current_dir(&cwd) // Point the crawler at the fake local repo. .env("MAVEN_REPO_LOCAL", &m2_repo) - // The Maven crawler is gated behind a runtime opt-in - // (`maven_runtime_enabled` in ecosystem_dispatch.rs); without - // this the crawl short-circuits to zero packages and the scan - // prints "No packages found." These tests are named for Maven - // *discovery*, so they must enable the real crawl path — otherwise - // they only ever exercise the disabled stub and pass vacuously. - .env("SOCKET_EXPERIMENTAL_MAVEN", "1") // Keep the run hermetic: no ambient token, no inherited repo path. .env_remove("SOCKET_API_TOKEN") .env_remove("SOCKET_CLI_API_TOKEN") diff --git a/crates/socket-patch-cli/tests/e2e_nuget.rs b/crates/socket-patch-cli/tests/e2e_nuget.rs index 5c3ec8ef..b103d8c3 100644 --- a/crates/socket-patch-cli/tests/e2e_nuget.rs +++ b/crates/socket-patch-cli/tests/e2e_nuget.rs @@ -63,12 +63,6 @@ async fn run(args: &[&str], cwd: &Path, nuget_packages: &Path, proxy_url: &str) .args(&arg_refs) .current_dir(&cwd) .env("NUGET_PACKAGES", &nuget_packages) - // The NuGet crawler is gated behind a runtime opt-in - // (`nuget_runtime_enabled()` → `SOCKET_EXPERIMENTAL_NUGET`). Without - // this, `scan` skips NuGet entirely and reports "No packages found.", - // which would silently defeat any discovery assertion. Enabling it here - // is what makes these tests actually exercise the NuGet code path. - .env("SOCKET_EXPERIMENTAL_NUGET", "1") .env_remove("SOCKET_API_TOKEN") .env_remove("SOCKET_CLI_API_TOKEN") .env_remove("SOCKET_API_URL") diff --git a/crates/socket-patch-cli/tests/e2e_safety_advisories.rs b/crates/socket-patch-cli/tests/e2e_safety_advisories.rs index b1b5939f..f01bf6f4 100644 --- a/crates/socket-patch-cli/tests/e2e_safety_advisories.rs +++ b/crates/socket-patch-cli/tests/e2e_safety_advisories.rs @@ -11,8 +11,7 @@ //! apply runs fully offline. //! 4. Invoke `socket-patch apply --json` with `--global-prefix` //! pointed at the package root, plus any per-ecosystem env -//! gates (e.g. `SOCKET_EXPERIMENTAL_NUGET=1`, -//! `NUGET_PACKAGES=`, `GOMODCACHE=`). +//! overrides (e.g. `NUGET_PACKAGES=`, `GOMODCACHE=`). //! 5. Parse the JSON envelope and assert the structured //! `envelope.sidecars[]` record matches the ecosystem's //! expected `code` / `severity` / `files[]` contract. @@ -448,10 +447,7 @@ fn nuget_apply_deletes_metadata_and_records_files() { let env = apply_and_parse( cwd, &packages, - &[ - ("NUGET_PACKAGES", packages.to_str().unwrap()), - ("SOCKET_EXPERIMENTAL_NUGET", "1"), - ], + &[("NUGET_PACKAGES", packages.to_str().unwrap())], ); // Patch landed. @@ -563,10 +559,7 @@ fn nuget_apply_with_non_utf8_filename_in_pkg_dir() { let env = apply_and_parse( cwd, &packages, - &[ - ("NUGET_PACKAGES", packages.to_str().unwrap()), - ("SOCKET_EXPERIMENTAL_NUGET", "1"), - ], + &[("NUGET_PACKAGES", packages.to_str().unwrap())], ); // Patch landed and .nupkg.metadata removal succeeded; the @@ -648,10 +641,7 @@ fn nuget_apply_with_metadata_directory_reports_sidecar_fixup_failed() { let env = apply_and_parse( cwd, &packages, - &[ - ("NUGET_PACKAGES", packages.to_str().unwrap()), - ("SOCKET_EXPERIMENTAL_NUGET", "1"), - ], + &[("NUGET_PACKAGES", packages.to_str().unwrap())], ); // Patch landed (atomic write commits before the sidecar runs). @@ -731,10 +721,7 @@ fn nuget_apply_signed_package_emits_files_and_advisory() { let env = apply_and_parse( cwd, &packages, - &[ - ("NUGET_PACKAGES", packages.to_str().unwrap()), - ("SOCKET_EXPERIMENTAL_NUGET", "1"), - ], + &[("NUGET_PACKAGES", packages.to_str().unwrap())], ); // Patch landed and the signature marker did NOT get clobbered. diff --git a/crates/socket-patch-cli/tests/ecosystem_dispatch_e2e.rs b/crates/socket-patch-cli/tests/ecosystem_dispatch_e2e.rs index abe10792..55770038 100644 --- a/crates/socket-patch-cli/tests/ecosystem_dispatch_e2e.rs +++ b/crates/socket-patch-cli/tests/ecosystem_dispatch_e2e.rs @@ -289,10 +289,6 @@ fn dispatch_branch_golang() { } #[test] -// Experimental ecosystem: the maven backend is unfinished, so this dispatch -// e2e is kept OFF the blocking CI suite (it must not gate progress on maven). -// Still compiled, and runnable on demand with `-- --ignored`. -#[ignore = "experimental ecosystem (maven): not gating CI until the maven backend is implemented; run with --ignored"] fn dispatch_branch_maven() { let tmp = tempfile::tempdir().unwrap(); write_root_package_json(tmp.path()); @@ -313,10 +309,6 @@ fn dispatch_branch_composer() { } #[test] -// Experimental ecosystem: the nuget backend is unfinished, so this dispatch -// e2e is kept OFF the blocking CI suite (it must not gate progress on nuget). -// Still compiled, and runnable on demand with `-- --ignored`. -#[ignore = "experimental ecosystem (nuget): not gating CI until the nuget backend is implemented; run with --ignored"] fn dispatch_branch_nuget() { let tmp = tempfile::tempdir().unwrap(); write_root_package_json(tmp.path()); @@ -482,9 +474,9 @@ fn run_rollback( cmd.arg("--global"); } cmd.current_dir(cwd); - // Scrub BEFORE seeding fixture envs: the fixture list includes - // SOCKET_-prefixed vars (SOCKET_EXPERIMENTAL_MAVEN/NUGET) that the - // prefix sweep would otherwise wipe (last env call per key wins). + // Scrub BEFORE seeding fixture envs, so a fixture-supplied + // SOCKET_-prefixed var survives the prefix sweep (last env call + // per key wins). scrub_socket_env(&mut cmd); for (k, v) in envs { cmd.env(k, v); @@ -779,9 +771,6 @@ fn rollback_dispatch_branch_golang() { } #[test] -// Experimental ecosystem (maven), kept OFF the blocking CI suite — see the -// note on `dispatch_branch_maven`. Run with `-- --ignored`. -#[ignore = "experimental ecosystem (maven): not gating CI until the maven backend is implemented; run with --ignored"] fn rollback_dispatch_branch_maven() { let tmp = tempfile::tempdir().unwrap(); let root = tmp.path(); @@ -799,10 +788,10 @@ fn rollback_dispatch_branch_maven() { let fixture = RollbackFixture { purl: purl.to_string(), verify_file, - envs: vec![ - ("MAVEN_REPO_LOCAL".to_string(), repo.display().to_string()), - ("SOCKET_EXPERIMENTAL_MAVEN".to_string(), "1".to_string()), - ], + envs: vec![( + "MAVEN_REPO_LOCAL".to_string(), + repo.display().to_string(), + )], global: false, }; assert_rollback_restored(root, "maven", &fixture); @@ -1000,18 +989,19 @@ fn setup_check_json_global_prefix_stdout_is_pure_json() { } #[test] -// Experimental ecosystem (nuget), kept OFF the blocking CI suite — see the -// note on `dispatch_branch_nuget`. This is the test that was failing in CI -// (the nuget rollback crawler discovers 0 packages). Run with -// `-- --ignored`. -#[ignore = "experimental ecosystem (nuget): not gating CI until the nuget backend is implemented; run with --ignored"] fn rollback_dispatch_branch_nuget() { let tmp = tempfile::tempdir().unwrap(); let root = tmp.path(); write_root_package_json(root); std::fs::write(root.join("app.csproj"), "\n").unwrap(); - // Legacy packages.config layout: /packages///. - let pkg = root.join("packages").join("Foo").join("1.0.0"); + // Legacy packages.config layout: /packages/./ + // (dotted, case-preserved — what nuget.exe actually writes there). + // The old `/` nesting this fixture used was neither + // the legacy layout nor the global-cache one (which lowercases both + // segments), so the crawler's lowercase probe only matched it on + // case-INSENSITIVE filesystems — the reason this test was red on + // Linux CI while it was `#[ignore]`d. + let pkg = root.join("packages").join("Foo.1.0.0"); std::fs::create_dir_all(pkg.join("lib")).unwrap(); let verify_file = pkg.join("lib").join("foo.dll"); std::fs::write(&verify_file, PATCHED).unwrap(); @@ -1020,7 +1010,7 @@ fn rollback_dispatch_branch_nuget() { let fixture = RollbackFixture { purl: purl.to_string(), verify_file, - envs: vec![("SOCKET_EXPERIMENTAL_NUGET".to_string(), "1".to_string())], + envs: vec![], global: false, }; assert_rollback_restored(root, "nuget", &fixture); diff --git a/crates/socket-patch-cli/tests/in_process_remote_ecosystems_apply.rs b/crates/socket-patch-cli/tests/in_process_remote_ecosystems_apply.rs index 08223aee..d14da4a5 100644 --- a/crates/socket-patch-cli/tests/in_process_remote_ecosystems_apply.rs +++ b/crates/socket-patch-cli/tests/in_process_remote_ecosystems_apply.rs @@ -279,10 +279,6 @@ async fn maven_handcrafted_install_apply_patches_file() { let after_hash = git_sha256(&patched); std::env::set_var("MAVEN_REPO_LOCAL", &repo); - // Maven crawler is runtime-gated behind this env var (see - // `ecosystem_dispatch::maven_runtime_enabled`). The test - // deliberately exercises the Maven apply path, so opt in. - std::env::set_var("SOCKET_EXPERIMENTAL_MAVEN", "1"); let server = MockServer::start().await; setup_apply_mock( @@ -312,7 +308,6 @@ async fn maven_handcrafted_install_apply_patches_file() { ); std::env::remove_var("MAVEN_REPO_LOCAL"); - std::env::remove_var("SOCKET_EXPERIMENTAL_MAVEN"); } /// Maven is the one release-variant ecosystem where multiple variants @@ -347,7 +342,6 @@ async fn maven_multi_classifier_patches_every_present_jar() { patched_b.extend_from_slice(b"\n# MARKER-B\n"); std::env::set_var("MAVEN_REPO_LOCAL", &repo); - std::env::set_var("SOCKET_EXPERIMENTAL_MAVEN", "1"); let base = "pkg:maven/org.example/native-lib@1.0.0"; let purl_a = format!("{base}?classifier=linux-x86_64&ext=jar"); @@ -434,7 +428,6 @@ async fn maven_multi_classifier_patches_every_present_jar() { ); std::env::remove_var("MAVEN_REPO_LOCAL"); - std::env::remove_var("SOCKET_EXPERIMENTAL_MAVEN"); } // --------------------------------------------------------------------------- @@ -536,7 +529,6 @@ async fn nuget_handcrafted_install_apply_patches_file() { // NuGet crawler is runtime-gated behind this env var (see // `ecosystem_dispatch::nuget_runtime_enabled`). The test // deliberately exercises the NuGet apply path, so opt in. - std::env::set_var("SOCKET_EXPERIMENTAL_NUGET", "1"); let server = MockServer::start().await; setup_apply_mock( @@ -566,7 +558,6 @@ async fn nuget_handcrafted_install_apply_patches_file() { ); std::env::remove_var("NUGET_PACKAGES"); - std::env::remove_var("SOCKET_EXPERIMENTAL_NUGET"); } // --------------------------------------------------------------------------- @@ -615,7 +606,6 @@ async fn maven_handcrafted_discovery() { std::fs::create_dir_all(&version_dir).unwrap(); std::fs::write(version_dir.join("foo-1.0.0.pom"), "").unwrap(); std::env::set_var("MAVEN_REPO_LOCAL", &repo); - std::env::set_var("SOCKET_EXPERIMENTAL_MAVEN", "1"); let server = MockServer::start().await; Mock::given(method("POST")) @@ -633,7 +623,6 @@ async fn maven_handcrafted_discovery() { // PURL queried — not that the crawler silently found nothing. assert_discovered_purl(&server, "pkg:maven/org.example/foo@1.0.0").await; std::env::remove_var("MAVEN_REPO_LOCAL"); - std::env::remove_var("SOCKET_EXPERIMENTAL_MAVEN"); } #[tokio::test] @@ -645,7 +634,6 @@ async fn nuget_handcrafted_discovery() { std::fs::create_dir_all(&dir).unwrap(); std::fs::write(dir.join("foo.nuspec"), "").unwrap(); std::env::set_var("NUGET_PACKAGES", &pkgs); - std::env::set_var("SOCKET_EXPERIMENTAL_NUGET", "1"); let server = MockServer::start().await; Mock::given(method("POST")) @@ -663,7 +651,6 @@ async fn nuget_handcrafted_discovery() { // its PURL queried — exit 0 alone would also pass an empty crawl. assert_discovered_purl(&server, "pkg:nuget/foo@1.0.0").await; std::env::remove_var("NUGET_PACKAGES"); - std::env::remove_var("SOCKET_EXPERIMENTAL_NUGET"); } // --------------------------------------------------------------------------- @@ -822,7 +809,6 @@ async fn maven_handcrafted_scan_apply_writes_manifest() { let after_hash = git_sha256(&patched); std::env::set_var("MAVEN_REPO_LOCAL", &repo); - std::env::set_var("SOCKET_EXPERIMENTAL_MAVEN", "1"); let purl = "pkg:maven/org.apache.commons/commons-lang3@3.12.0"; let uuid = "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee"; @@ -856,7 +842,6 @@ async fn maven_handcrafted_scan_apply_writes_manifest() { assert_manifest_records(tmp.path(), purl, uuid); std::env::remove_var("MAVEN_REPO_LOCAL"); - std::env::remove_var("SOCKET_EXPERIMENTAL_MAVEN"); } #[tokio::test] @@ -881,7 +866,6 @@ async fn nuget_handcrafted_scan_apply_writes_manifest() { let after_hash = git_sha256(&patched); std::env::set_var("NUGET_PACKAGES", &packages); - std::env::set_var("SOCKET_EXPERIMENTAL_NUGET", "1"); let purl = "pkg:nuget/Newtonsoft.Json@13.0.3"; let uuid = "dfdfdfdf-dfdf-4fdf-8fdf-dfdfdfdfdfdf"; @@ -914,7 +898,6 @@ async fn nuget_handcrafted_scan_apply_writes_manifest() { assert_manifest_records(tmp.path(), purl, uuid); std::env::remove_var("NUGET_PACKAGES"); - std::env::remove_var("SOCKET_EXPERIMENTAL_NUGET"); } // Helper kept around so `PathBuf` import is used in case of future tests. diff --git a/crates/socket-patch-cli/tests/in_process_rollback_all_ecosystems.rs b/crates/socket-patch-cli/tests/in_process_rollback_all_ecosystems.rs index 4e28f5d9..f65101a3 100644 --- a/crates/socket-patch-cli/tests/in_process_rollback_all_ecosystems.rs +++ b/crates/socket-patch-cli/tests/in_process_rollback_all_ecosystems.rs @@ -476,13 +476,10 @@ async fn rollback_maven_restores_original_content() { ); std::env::set_var("MAVEN_REPO_LOCAL", &repo); - // Maven crawler is runtime-gated; opt in for the test. - std::env::set_var("SOCKET_EXPERIMENTAL_MAVEN", "1"); let mut args = default_rollback_args(tmp.path(), "maven"); args.common.global = true; let code = rollback_run(args).await; std::env::remove_var("MAVEN_REPO_LOCAL"); - std::env::remove_var("SOCKET_EXPERIMENTAL_MAVEN"); assert_eq!(code, 0, "maven rollback must report success (exit 0)"); assert_eq!( @@ -586,13 +583,10 @@ async fn rollback_nuget_restores_original_content() { ); std::env::set_var("NUGET_PACKAGES", &packages); - // NuGet crawler is runtime-gated; opt in for the test. - std::env::set_var("SOCKET_EXPERIMENTAL_NUGET", "1"); let mut args = default_rollback_args(tmp.path(), "nuget"); args.common.global = true; let code = rollback_run(args).await; std::env::remove_var("NUGET_PACKAGES"); - std::env::remove_var("SOCKET_EXPERIMENTAL_NUGET"); assert_eq!(code, 0, "nuget rollback must report success (exit 0)"); assert_eq!( diff --git a/crates/socket-patch-cli/tests/in_process_scan.rs b/crates/socket-patch-cli/tests/in_process_scan.rs index 44abfa8f..622d2cb4 100644 --- a/crates/socket-patch-cli/tests/in_process_scan.rs +++ b/crates/socket-patch-cli/tests/in_process_scan.rs @@ -1367,3 +1367,69 @@ async fn scan_non_json_dry_run_does_not_mutate() { "non-JSON scan must fetch patch details before the dry-run stop" ); } + +// --------------------------------------------------------------------------- +// Maven/NuGet are first-class: every scan mode discovers them. +// --------------------------------------------------------------------------- + +/// Maven and NuGet used to sit behind `SOCKET_EXPERIMENTAL_MAVEN` / +/// `SOCKET_EXPERIMENTAL_NUGET` runtime gates that silently dropped them +/// from discovery. The gates are retired: every scan mode (default, +/// `--mode hosted`, `--mode vendored`) must crawl both with no opt-in of +/// any kind. The oracle is the batch POST body — it carries exactly the +/// purls the crawl discovered, so a resurrected gate shows up as the +/// purls (the only installed packages) going missing. +#[tokio::test] +#[serial] +async fn scan_discovers_maven_and_nuget_in_every_mode() { + use socket_patch_cli::commands::scan::ScanMode; + + const MAVEN_PURL: &str = "pkg:maven/org.example/foo@1.0.0"; + const NUGET_PURL: &str = "pkg:nuget/Foo@1.0.0"; + + let tmp = tempfile::tempdir().unwrap(); + write_root_package_json(tmp.path()); + // Maven: java-project marker + a local repository the crawler reaches + // via MAVEN_REPO_LOCAL (verified by the version dir's `.pom`). + std::fs::write(tmp.path().join("pom.xml"), "\n").unwrap(); + let artifact_dir = tmp.path().join("m2repo/org/example/foo/1.0.0"); + std::fs::create_dir_all(&artifact_dir).unwrap(); + std::fs::write(artifact_dir.join("foo-1.0.0.pom"), "").unwrap(); + // NuGet: project marker + legacy `packages/./` layout + // (verified by the `.nuspec`) — reachable without any env override. + std::fs::write(tmp.path().join("app.csproj"), "\n").unwrap(); + let nupkg_dir = tmp.path().join("packages/Foo.1.0.0"); + std::fs::create_dir_all(&nupkg_dir).unwrap(); + std::fs::write(nupkg_dir.join("Foo.nuspec"), "").unwrap(); + + std::env::set_var("MAVEN_REPO_LOCAL", tmp.path().join("m2repo")); + + for mode in [None, Some(ScanMode::Hosted), Some(ScanMode::Vendored)] { + let server = MockServer::start().await; + mock_batch_empty(&server).await; + + let mut args = default_args(tmp.path()); + args.common.api_url = Some(server.uri()); + args.mode = mode; + + assert_eq!(run_scrubbed(args).await, 0, "mode {mode:?} must exit 0"); + + let reqs = recorded(&server).await; + let posts = batch_posts(&reqs); + assert_eq!( + posts.len(), + 1, + "mode {mode:?} must query the batch API once for the crawled packages" + ); + let body = req_body(posts[0]); + assert!( + body.contains(MAVEN_PURL), + "mode {mode:?} must discover maven with no opt-in; body: {body}" + ); + assert!( + body.contains(NUGET_PURL), + "mode {mode:?} must discover nuget with no opt-in; body: {body}" + ); + } + std::env::remove_var("MAVEN_REPO_LOCAL"); +} diff --git a/crates/socket-patch-cli/tests/setup_matrix_maven.rs b/crates/socket-patch-cli/tests/setup_matrix_maven.rs index f2a121e7..b6a914de 100644 --- a/crates/socket-patch-cli/tests/setup_matrix_maven.rs +++ b/crates/socket-patch-cli/tests/setup_matrix_maven.rs @@ -1,7 +1,5 @@ -//! setup-matrix: maven ecosystem (mvn). No native post-install hook, -//! `setup` is a no-op, and apply is additionally gated behind -//! `SOCKET_EXPERIMENTAL_MAVEN` (the driver sets it). The with-setup -//! cases are an EXPECTED BASELINE GAP. +//! setup-matrix: maven ecosystem (mvn). No native post-install hook and +//! `setup` is a no-op. The with-setup cases are an EXPECTED BASELINE GAP. //! //! IMPORTANT — why this file carries a real assertion of its own: //! `smc::run_pm("maven", "mvn")` routes maven through the shared Docker @@ -101,16 +99,13 @@ mod host_guard { /// runs; a (perfectly valid!) ambient `SOCKET_SETUP_EXCLUDE` stands in for /// `setup --exclude`, which a real `setup` run PERSISTS — creating /// `.socket/manifest.json` inside the maven fixture and failing - /// `assert_pristine`; and an enabled `SOCKET_EXPERIMENTAL_MAVEN` gate in - /// the shell/CI could quietly change maven's surface behind the test's - /// back. (Safe to set process-wide: the only other test in this binary is - /// the `#[ignore]`d matrix pass, which routes through + /// `assert_pristine`. (Safe to set process-wide: the only other test in + /// this binary is the `#[ignore]`d matrix pass, which routes through /// `smc::host_driver_command`'s own `SOCKET_*` prefix scrub.) const HOSTILE_DECOYS: &[(&str, &str)] = &[ ("SOCKET_STRICT", "banana"), ("SOCKET_VENDOR_SOURCE", "bogus-decoy"), ("SOCKET_SETUP_EXCLUDE", "decoy-member"), - ("SOCKET_EXPERIMENTAL_MAVEN", "true"), ]; /// Absolute path to the binary under test, via cargo's `CARGO_BIN_EXE_*`. diff --git a/crates/socket-patch-cli/tests/setup_matrix_nuget.rs b/crates/socket-patch-cli/tests/setup_matrix_nuget.rs index c6f9bcea..924407bc 100644 --- a/crates/socket-patch-cli/tests/setup_matrix_nuget.rs +++ b/crates/socket-patch-cli/tests/setup_matrix_nuget.rs @@ -1,7 +1,5 @@ -//! setup-matrix: nuget ecosystem (dotnet). No native post-install hook, -//! `setup` is a no-op, and apply is additionally gated behind -//! `SOCKET_EXPERIMENTAL_NUGET` (the driver sets it). The with-setup -//! cases are an EXPECTED BASELINE GAP. +//! setup-matrix: nuget ecosystem (dotnet). No native post-install hook and +//! `setup` is a no-op. The with-setup cases are an EXPECTED BASELINE GAP. //! //! IMPORTANT — why this file carries a real assertion of its own: //! `smc::run_pm("nuget", "dotnet")` routes nuget through the shared Docker @@ -94,17 +92,14 @@ mod host_guard { /// runs; and a (perfectly valid!) ambient `SOCKET_SETUP_EXCLUDE` stands /// in for `setup --exclude`, which a real `setup` run PERSISTS — /// creating `.socket/manifest.json` inside the dotnet fixture and - /// failing the final only-the-csproj assertion. `SOCKET_EXPERIMENTAL_NUGET` - /// rides along so the experimental gate can never quietly change nuget's - /// surface behind the test's back. (Safe to set process-wide: the only - /// other test in this binary is the `#[ignore]`d matrix pass, which - /// routes through `smc::host_driver_command`'s own `SOCKET_*` prefix - /// scrub.) + /// failing the final only-the-csproj assertion. (Safe to set + /// process-wide: the only other test in this binary is the `#[ignore]`d + /// matrix pass, which routes through `smc::host_driver_command`'s own + /// `SOCKET_*` prefix scrub.) const HOSTILE_DECOYS: &[(&str, &str)] = &[ ("SOCKET_STRICT", "banana"), ("SOCKET_VENDOR_SOURCE", "bogus-decoy"), ("SOCKET_SETUP_EXCLUDE", "decoy-member"), - ("SOCKET_EXPERIMENTAL_NUGET", "true"), ]; /// Absolute path to the binary under test, via cargo's `CARGO_BIN_EXE_*`. diff --git a/crates/socket-patch-core/Cargo.toml b/crates/socket-patch-core/Cargo.toml index 49f8ea88..2a1d5d4c 100644 --- a/crates/socket-patch-core/Cargo.toml +++ b/crates/socket-patch-core/Cargo.toml @@ -40,10 +40,9 @@ libc = { workspace = true } self-replace = { workspace = true } # All ecosystems (npm, PyPI, Ruby gems, Go, Cargo, NuGet, Maven, Composer, -# Deno) are unconditionally compiled in — there are no ecosystem feature -# gates. Maven `apply` stays runtime-gated behind `SOCKET_EXPERIMENTAL_MAVEN=1` -# (in-place jar patching corrupts sidecars); committable `vendor` is safe (it -# never touches ~/.m2). +# Deno) are unconditionally compiled in AND enabled at runtime — there are +# no ecosystem feature gates and no runtime env gates (the old +# SOCKET_EXPERIMENTAL_MAVEN/NUGET opt-ins are retired). [dev-dependencies] tempfile = { workspace = true } diff --git a/crates/socket-patch-core/tests/crawler_nuget_e2e.rs b/crates/socket-patch-core/tests/crawler_nuget_e2e.rs index a749fe6b..97ba15d5 100644 --- a/crates/socket-patch-core/tests/crawler_nuget_e2e.rs +++ b/crates/socket-patch-core/tests/crawler_nuget_e2e.rs @@ -1,6 +1,6 @@ //! Integration coverage for `crawlers::nuget_crawler`. The //! apply-CLI suite drives the global-cache `find_by_purls` happy -//! path with `SOCKET_EXPERIMENTAL_NUGET=1`; everything else here — +//! path; everything else here — //! legacy `Packages/.` layout, case-insensitive //! lookup, `crawl_all` directory scanning, `scan_package_dir`'s //! hidden-dir skip, `get_nuget_package_paths` discovery branches — diff --git a/docs/ecosystems.md b/docs/ecosystems.md index f9e1599d..1313c4eb 100644 --- a/docs/ecosystems.md +++ b/docs/ecosystems.md @@ -19,15 +19,19 @@ The backticked slug in each row is the value `-e`/`--ecosystems` accepts (e.g. | Cargo (`cargo`) | ✅ in-place + `.cargo-checksum.json` rewrite (shared registry-cache caveat — see [Cargo: shared registry cache](#cargo-shared-registry-cache)) | ✅ `[patch.crates-io]` path entry | ✅ per-patch sparse registry (`[registries.socket-patch-]` + Cargo.lock source/checksum) | | RubyGems (`gem`) | ✅ Bundler plugin via `setup` | ✅ Gemfile + Gemfile.lock path pair | ✅ per-dep `source` block; the `CHECKSUMS` pin needs bundler ≥ 2.6 (older locks get a `redirect_gem_no_checksums_section` warning) | | Go (`golang`) | ✅ `go.mod` `replace` → `.socket/go-patches/` — see [Go: directory replaces and go.sum](#go-directory-replaces-and-gosum) | ✅ `replace` → the committed vendor tree | ❌ **not possible** — sumdb, module-path identity, and default-GOPROXY leakage each rule it out; see [golang-hosted-no-go.md](design/golang-hosted-no-go.md). **Use vendored** (`redirect_golang_unsupported` names the remedy) | -| Maven (`maven`) | ⚠️ experimental, apply-only (no `setup` hook — reports `no_files`) — gated behind `SOCKET_EXPERIMENTAL_MAVEN=1` (in-place jar patching corrupts the `~/.m2` checksum sidecars); prefer vendored / hosted | ✅ committed maven2 `file://` repository. A root pom declaring `` (multi-module aggregator) is refused (`vendor_maven_multimodule_unsupported`), and a gradle-only project is refused (`vendor_gradle_unsupported`) | ✅ **pom projects only, fail-closed** — the patched jar is pinned at a Socket-only `-socket.` suffix; `${property}` versions are refused; Gradle gets a manual `exclusiveContent` snippet — see [Maven & NuGet caveats](#maven--nuget-caveats) | -| NuGet (`nuget`) | ⚠️ experimental, apply-only (no `setup` hook — reports `no_files`) — gated behind `SOCKET_EXPERIMENTAL_NUGET=1` (in-place patching breaks the `.nupkg.sha512` tamper-evidence sidecar); prefer vendored / hosted | ✅ committed folder feed + `packageSourceMapping` + `packages.lock.json` contentHash pin | ✅ `nuget.config` source + source-mapping, `packages.lock.json` contentHash rewrite. See the locked-mode note in [Maven & NuGet caveats](#maven--nuget-caveats) | +| Maven (`maven`) | ✅ apply-only (no `setup` hook — reports `no_files`); in-place jar patching leaves the `~/.m2` checksum sidecars stale — prefer vendored / hosted, see [Maven & NuGet caveats](#maven--nuget-caveats) | ✅ committed maven2 `file://` repository. A root pom declaring `` (multi-module aggregator) is refused (`vendor_maven_multimodule_unsupported`), and a gradle-only project is refused (`vendor_gradle_unsupported`) | ✅ **pom projects only, fail-closed** — the patched jar is pinned at a Socket-only `-socket.` suffix; `${property}` versions are refused; Gradle gets a manual `exclusiveContent` snippet — see [Maven & NuGet caveats](#maven--nuget-caveats) | +| NuGet (`nuget`) | ✅ apply-only (no `setup` hook — reports `no_files`); in-place patching deletes `.nupkg.metadata` and advises on the `.nupkg.sha512` tamper-evidence sidecar — prefer vendored / hosted, see [Maven & NuGet caveats](#maven--nuget-caveats) | ✅ committed folder feed + `packageSourceMapping` + `packages.lock.json` contentHash pin | ✅ `nuget.config` source + source-mapping, `packages.lock.json` contentHash rewrite. See the locked-mode note in [Maven & NuGet caveats](#maven--nuget-caveats) | | Composer (`composer`) | ✅ post-install script events | ✅ `composer.lock` `dist: path` rewrite | ✅ `composer.lock` dist url + shasum rewrite | | Deno (`deno`) | ✅ apply-only — no install hook (`setup` reports `no_files`); declare in `setup.manual` for VEX coverage | ❌ refused (`vendor_unsupported_ecosystem`) | ❌ not supported | -> **Maven / NuGet discovery gate**: discovering *installed* Maven and NuGet packages (the -> crawl behind `scan` / `apply` / `vendor`) currently requires the same -> `SOCKET_EXPERIMENTAL_MAVEN=1` / `SOCKET_EXPERIMENTAL_NUGET=1` opt-in in every mode. The -> vendored/hosted wiring itself is safe — the gate guards the agent-mode sidecar risk. +> **Maven / NuGet sidecar caveat**: Maven and NuGet are fully enabled in every mode (the +> old `SOCKET_EXPERIMENTAL_MAVEN` / `SOCKET_EXPERIMENTAL_NUGET` opt-ins are retired). +> In-place (agent-mode) patching leaves the caches' own checksum sidecars stale: NuGet's +> post-apply fixup deletes `.nupkg.metadata` and raises an advisory for the +> signed-package `.nupkg.sha512` tamper marker it cannot honestly rewrite; Maven's +> `.jar.sha1`/`.jar.md5` are left as-is. The copy-out modes — `vendor`, +> `scan --mode vendored`, `scan --mode hosted` — never write into the caches and avoid +> the issue entirely. ## npm hosted-mode notes diff --git a/tests/setup_matrix/run-case.sh b/tests/setup_matrix/run-case.sh index 13c21959..c399b675 100755 --- a/tests/setup_matrix/run-case.sh +++ b/tests/setup_matrix/run-case.sh @@ -657,10 +657,8 @@ fi # NOTE: SOCKET_OFFLINE/SOCKET_FORCE must be "true"/"false" — the apply # `--force` flag (unlike its siblings) has no boolish value parser, so # SOCKET_FORCE=1 is rejected with "invalid value '1' for '--force'". -# The SOCKET_EXPERIMENTAL_* gates are read directly from the env and use -# "1". export SOCKET_OFFLINE=true SOCKET_FORCE=true SOCKET_API_TOKEN=fake SOCKET_ORG_SLUG=test-org -export SOCKET_TELEMETRY_DISABLED=1 SOCKET_EXPERIMENTAL_MAVEN=1 SOCKET_EXPERIMENTAL_NUGET=1 +export SOCKET_TELEMETRY_DISABLED=1 # Isolate the pypi `.pth` hook's change-detection stamp per case so runs # don't bleed into each other (the stamp lives under XDG_CACHE_HOME). export XDG_CACHE_HOME="$WORKDIR/.cache"