Skip to content

Commit 44b25ff

Browse files
committed
Merge origin/main into agent/fix-vlt-brotli-node-flag
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NhRxWtzEYpLyrByBegiiRy
2 parents 7535c05 + 4646693 commit 44b25ff

7 files changed

Lines changed: 853 additions & 66 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ Recognition rules that hold for every ecosystem:
387387
|---|---|---|
388388
| Vendored: a lockfile/config wires a `.socket/vendor` artifact, or a live vendor ledger entry | The **committed artifact** is hashed against the record's `afterHash`. The ledger entry is used when it names the wired artifact (it carries the dir-artifact inventory); otherwise an entry is synthesized from the reference. A present installed tree with different bytes only warns `vendored_tree_out_of_sync`. | `(vendored)` |
389389
| Hosted: a discovered patch-host reference (or a live pre-v5 redirect-ledger record) | The installed copies the build **consumes** through the hosted wiring are hash-verified when any exist: the Go replacement module, never the pristine `M@v` in the module cache; the Socket-registry cargo source dir; maven's suffixed version. Installed evidence wins: `hash_mismatch` / `not_applied` are omitted. With **nothing installed**, a discovered reference whose lock pins the artifact (or whose format's rewriter never writes a pin) attests from that pin, which is the same evidence as in-run `scan --mode hosted --vex`. A pre-v5 ledger-only record, or a reference whose required pin is missing, stays `package_not_found`. So do purls that `--ecosystems` kept out of the crawl, because "not installed" has to mean the crawler looked. The same goes for npm purls when an installed pnpm tree records its virtual store outside the project (`enableGlobalVirtualStore`, or a `virtualStoreDir` that climbs out): transitive deps there are invisible to the crawler. A pnpm `modulesDir` inside the project is crawled. | `(redirected)` |
390-
| Agent: a manifest record with no live hosted/vendored wiring | The installed tree, unchanged. **Every** installed copy the crawler finds for the purl (npm nests duplicates of one `name@version`) must hash to the patched bytes, as `apply` patches every copy. One unpatched copy omits the purl with that copy's tag (`not_applied` / `hash_mismatch`). | none |
390+
| Agent: a manifest record with no live hosted/vendored wiring | The installed tree, unchanged. **Every** installed copy the crawler finds for the purl (npm nests duplicates of one `name@version`; pnpm, vlt, Bun and Deno stores add peer-variant copies and copies bundled inside other packages) must hash to the patched bytes, as `apply` patches every copy. One unpatched copy omits the purl with that copy's tag (`not_applied` / `hash_mismatch`). | none |
391391

392392
**Liveness gates.** These gates run before hashing, and `--no-verify` / `--vex-no-verify` skips only the hashing, never the gates:
393393

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

Lines changed: 242 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@
4040
use std::collections::{BTreeMap, HashMap};
4141
use std::path::{Path, PathBuf};
4242

43-
use socket_patch_core::crawlers::npm_crawler::find_store_peer_variant_copies;
43+
#[cfg(not(test))]
44+
use socket_patch_core::crawlers::npm_crawler::with_store_peer_variant_copies;
4445
use socket_patch_core::crawlers::{
4546
CargoCrawler, CrawlerOptions, Ecosystem, GoCrawler, MavenCrawler, NpmCrawler,
4647
};
@@ -50,6 +51,8 @@ use socket_patch_core::vendor::go_mod_edit::{
5051
};
5152
use socket_patch_core::vendor::lock_inventory::LockIntegrity;
5253
use socket_patch_core::vex::HostedCopies;
54+
#[cfg(test)]
55+
use tests::recording_store_variants as with_store_peer_variant_copies;
5356

5457
use crate::args::GlobalArgs;
5558
use crate::commands::vex_sources::HostedWiring;
@@ -61,8 +64,9 @@ use crate::ecosystem_dispatch::{
6164
/// module docs), under the same crawler options and `--ecosystems` scope as
6265
/// the installed-tree lookup. `installed` is that lookup's every-copy
6366
/// result ([`crate::ecosystem_dispatch::find_manifest_package_copies_reusing`] over
64-
/// the record view, which holds every hosted purl): the shared-location
65-
/// ecosystems read it instead of crawling the tree a second time. `prior`
67+
/// the record view, which holds every hosted purl), including npm store
68+
/// variants. The shared-location ecosystems read it instead of crawling
69+
/// the tree a second time. `prior`
6670
/// (embedded hosted `scan --vex` only) is scan's npm crawl of the same
6771
/// tree: the alias walk takes its `node_modules` roots and the identity
6872
/// fallback its packages instead of walking the tree again.
@@ -109,13 +113,38 @@ pub(crate) async fn hosted_consumed_copies(
109113
let mut paths = all.remove(purl).unwrap_or_default();
110114
// The installed-tree lookup already resolves importer-tree
111115
// aliases, so most of the walk's finds are in `paths` already.
112-
for alias in aliases.remove(purl).unwrap_or_default() {
113-
if !paths.contains(&alias) {
114-
paths.push(alias);
116+
let extra: Vec<PathBuf> = aliases
117+
.remove(purl)
118+
.unwrap_or_default()
119+
.into_iter()
120+
.filter(|alias| !paths.contains(alias))
121+
.collect();
122+
if npm.contains(&purl) && installed.get(purl).is_some_and(|p| !p.is_empty()) {
123+
// The installed lookup already expanded these copies.
124+
// Expanding its N variants again scans the store N times.
125+
// Only aliases are new; expand them before merging so a
126+
// different alias/store can still contribute more copies.
127+
if !extra.is_empty() {
128+
let added = with_store_peer_variant_copies(extra).await;
129+
let mut seen = std::collections::HashSet::new();
130+
for path in &paths {
131+
seen.insert(tokio::fs::canonicalize(path).await.unwrap_or(path.clone()));
132+
}
133+
for path in added {
134+
let canonical =
135+
tokio::fs::canonicalize(&path).await.unwrap_or(path.clone());
136+
if seen.insert(canonical) {
137+
paths.push(path);
138+
}
139+
}
115140
}
116-
}
117-
if npm.contains(&purl) {
118-
paths = with_store_variants(paths).await;
141+
} else if npm.contains(&purl) {
142+
// No installed copies: the identity fallback and aliases
143+
// have not had their store variants enumerated yet.
144+
paths.extend(extra);
145+
paths = with_store_peer_variant_copies(paths).await;
146+
} else {
147+
paths.extend(extra);
119148
}
120149
out.insert(
121150
purl.clone(),
@@ -321,28 +350,6 @@ async fn npm_identity_fallback_reusing(
321350
}
322351
}
323352

324-
/// `paths` plus every store variant of each (a pnpm peer suffix, a vlt peer
325-
/// or modifier extra, a vlt registry-alias instance of the same
326-
/// `name@version`): the crawler resolves a store copy only for a package
327-
/// with no importer copy, leaving the variants to apply's fan-out, but each
328-
/// variant is what some dependent loads.
329-
async fn with_store_variants(paths: Vec<PathBuf>) -> Vec<PathBuf> {
330-
let mut seen: std::collections::HashSet<PathBuf> = std::collections::HashSet::new();
331-
for path in &paths {
332-
seen.insert(tokio::fs::canonicalize(path).await.unwrap_or(path.clone()));
333-
}
334-
let mut out = paths.clone();
335-
for path in &paths {
336-
for copy in find_store_peer_variant_copies(path).await {
337-
let canonical = tokio::fs::canonicalize(&copy).await.unwrap_or(copy.clone());
338-
if seen.insert(canonical) {
339-
out.push(copy);
340-
}
341-
}
342-
}
343-
out
344-
}
345-
346353
// ── golang ───────────────────────────────────────────────────────────────
347354

348355
/// Under `replace M v => patch.socket.dev/gopatch/<uuid> <sver>` the build
@@ -604,6 +611,208 @@ async fn maven_copies(options: &CrawlerOptions, purl: &str, wiring: &HostedWirin
604611
mod tests {
605612
use super::*;
606613

614+
tokio::task_local! {
615+
// Observe real expansion work only in the regression's own task;
616+
// concurrent tests keep calling the production helper normally.
617+
static VARIANT_INPUTS: std::cell::RefCell<Vec<Vec<PathBuf>>>;
618+
}
619+
620+
pub(super) async fn recording_store_variants(paths: Vec<PathBuf>) -> Vec<PathBuf> {
621+
let _ = VARIANT_INPUTS.try_with(|calls| calls.borrow_mut().push(paths.clone()));
622+
socket_patch_core::crawlers::npm_crawler::with_store_peer_variant_copies(paths).await
623+
}
624+
625+
#[cfg(unix)]
626+
async fn tracked_npm_hosted(
627+
common: &GlobalArgs,
628+
installed: &HashMap<String, Vec<PathBuf>>,
629+
) -> (Vec<PathBuf>, Vec<Vec<PathBuf>>) {
630+
let purl = "pkg:npm/left-pad@1.3.0".to_string();
631+
let hosted = BTreeMap::from([(
632+
purl.clone(),
633+
HostedWiring {
634+
uuid: "11111111-1111-4111-8111-111111111111".to_string(),
635+
refs: Vec::new(),
636+
},
637+
)]);
638+
VARIANT_INPUTS
639+
.scope(std::cell::RefCell::new(Vec::new()), async {
640+
let mut found = hosted_consumed_copies(common, &hosted, installed, None).await;
641+
let paths = found.remove(&purl).unwrap().paths;
642+
let calls = VARIANT_INPUTS.with(|inputs| inputs.borrow().clone());
643+
(paths, calls)
644+
})
645+
.await
646+
}
647+
648+
#[cfg(unix)]
649+
fn peer_copies(store: &Path, count: usize) -> Vec<PathBuf> {
650+
(0..count)
651+
.map(|i| {
652+
let path = store.join(format!(
653+
"left-pad@1.3.0(peer@1.0.{i})/node_modules/left-pad"
654+
));
655+
pkg(&path, "left-pad", "1.3.0");
656+
path
657+
})
658+
.collect()
659+
}
660+
661+
#[cfg(unix)]
662+
#[tokio::test]
663+
async fn hosted_reuses_expanded_npm_copies_and_merges_alias_variants() {
664+
let tmp = tempfile::tempdir().unwrap();
665+
let nm = tmp.path().canonicalize().unwrap().join("node_modules");
666+
let peers = peer_copies(&nm.join(".pnpm"), 8);
667+
std::os::unix::fs::symlink(&peers[0], nm.join("left-pad")).unwrap();
668+
let common = GlobalArgs {
669+
cwd: tmp.path().canonicalize().unwrap(),
670+
ecosystems: Some(vec!["npm".to_string()]),
671+
..GlobalArgs::default()
672+
};
673+
let purl = "pkg:npm/left-pad@1.3.0".to_string();
674+
let installed = crate::ecosystem_dispatch::find_manifest_package_copies_reusing(
675+
std::slice::from_ref(&purl),
676+
&common,
677+
true,
678+
None,
679+
)
680+
.await;
681+
assert_eq!(installed[&purl].len(), peers.len());
682+
let (paths, calls) = tracked_npm_hosted(&common, &installed).await;
683+
assert_eq!(paths, installed[&purl]);
684+
assert!(
685+
calls.is_empty(),
686+
"already-expanded copies were rescanned: {calls:?}"
687+
);
688+
689+
// A real alias is absent from the name-keyed installed set. Its
690+
// store variants overlap that set canonically, including the
691+
// importer link's physical copy; keep the alias once and preserve
692+
// the original importer-first path choices.
693+
let alias = nm.join("lp");
694+
pkg(&alias, "left-pad", "1.3.0");
695+
let (paths, calls) = tracked_npm_hosted(&common, &installed).await;
696+
assert_eq!(calls, vec![vec![alias.clone()]]);
697+
let mut expected = installed[&purl].clone();
698+
expected.push(alias);
699+
assert_eq!(paths, expected);
700+
701+
// An alias beneath a real nested host can reach another store.
702+
// The installed root copy makes the name-keyed resolver skip
703+
// those peers, so alias expansion must still add them even when
704+
// installed copies are already present.
705+
let host = nm.join("host");
706+
pkg(&host, "host", "1.0.0");
707+
let host_nm = host.join("node_modules");
708+
let nested_peers = peer_copies(&host_nm.join(".pnpm"), 2);
709+
let nested_alias = host_nm.join("lp");
710+
pkg(&nested_alias, "left-pad", "1.3.0");
711+
let installed_again = crate::ecosystem_dispatch::find_manifest_package_copies_reusing(
712+
std::slice::from_ref(&purl),
713+
&common,
714+
true,
715+
None,
716+
)
717+
.await;
718+
assert_eq!(installed_again, installed);
719+
let (paths, calls) = tracked_npm_hosted(&common, &installed_again).await;
720+
assert_eq!(calls.len(), 1);
721+
let mut inputs = calls[0].clone();
722+
inputs.sort();
723+
let mut aliases = vec![nm.join("lp"), nested_alias.clone()];
724+
aliases.sort();
725+
assert_eq!(inputs, aliases);
726+
assert_eq!(&paths[..installed[&purl].len()], installed[&purl]);
727+
expected.push(nested_alias);
728+
expected.extend(nested_peers);
729+
let mut actual = paths.clone();
730+
actual.sort();
731+
expected.sort();
732+
assert_eq!(actual, expected);
733+
assert_eq!(
734+
paths
735+
.iter()
736+
.map(|path| path.canonicalize().unwrap())
737+
.collect::<std::collections::HashSet<_>>()
738+
.len(),
739+
paths.len()
740+
);
741+
}
742+
743+
#[cfg(unix)]
744+
#[tokio::test]
745+
async fn hosted_expands_alias_only_copies() {
746+
let tmp = tempfile::tempdir().unwrap();
747+
let store = tmp
748+
.path()
749+
.canonicalize()
750+
.unwrap()
751+
.join("node_modules/.pnpm");
752+
let peers = peer_copies(&store, 2);
753+
// Run within a store package whose nested dependency is an alias.
754+
// The sibling peer copies are outside its project-root search.
755+
let root = store.join("host@1.0.0/node_modules/host");
756+
let alias = root.join("node_modules/lp");
757+
pkg(&alias, "left-pad", "1.3.0");
758+
let common = GlobalArgs {
759+
cwd: root,
760+
ecosystems: Some(vec!["npm".to_string()]),
761+
..GlobalArgs::default()
762+
};
763+
let purl = "pkg:npm/left-pad@1.3.0".to_string();
764+
let installed = crate::ecosystem_dispatch::find_manifest_package_copies_reusing(
765+
std::slice::from_ref(&purl),
766+
&common,
767+
true,
768+
None,
769+
)
770+
.await;
771+
assert!(installed.is_empty(), "{installed:?}");
772+
let (mut paths, calls) = tracked_npm_hosted(&common, &installed).await;
773+
assert_eq!(calls, vec![vec![alias.clone()]]);
774+
let mut expected = peers;
775+
expected.push(alias);
776+
paths.sort();
777+
expected.sort();
778+
assert_eq!(paths, expected);
779+
}
780+
781+
#[cfg(unix)]
782+
#[tokio::test]
783+
async fn hosted_expands_identity_fallback_with_empty_installed_entry() {
784+
let tmp = tempfile::tempdir().unwrap();
785+
let peers = peer_copies(
786+
&tmp.path()
787+
.canonicalize()
788+
.unwrap()
789+
.join("external/node_modules/.pnpm"),
790+
2,
791+
);
792+
let root = tmp.path().canonicalize().unwrap().join("project");
793+
let alias = root.join("node_modules/lp");
794+
std::fs::create_dir_all(alias.parent().unwrap()).unwrap();
795+
std::os::unix::fs::symlink(&peers[0], &alias).unwrap();
796+
let common = GlobalArgs {
797+
cwd: root,
798+
ecosystems: Some(vec!["npm".to_string()]),
799+
..GlobalArgs::default()
800+
};
801+
let purl = "pkg:npm/left-pad@1.3.0".to_string();
802+
assert!(
803+
npm_alias_copies(&common.crawler_options(), std::slice::from_ref(&purl))
804+
.await
805+
.is_empty()
806+
);
807+
let installed = HashMap::from([(purl, Vec::new())]);
808+
let (mut paths, calls) = tracked_npm_hosted(&common, &installed).await;
809+
assert_eq!(calls, vec![vec![alias.clone()]]);
810+
let mut expected = vec![alias, peers[1].clone()];
811+
paths.sort();
812+
expected.sort();
813+
assert_eq!(paths, expected);
814+
}
815+
607816
fn pkg(dir: &Path, name: &str, version: &str) {
608817
std::fs::create_dir_all(dir).unwrap();
609818
std::fs::write(
@@ -857,7 +1066,7 @@ mod tests {
8571066
nm.join("left-pad"),
8581067
)
8591068
.unwrap();
860-
let mut got = with_store_variants(vec![nm.join("left-pad")]).await;
1069+
let mut got = with_store_peer_variant_copies(vec![nm.join("left-pad")]).await;
8611070
got.sort();
8621071
let mut want = vec![
8631072
nm.join("left-pad"),
@@ -866,7 +1075,7 @@ mod tests {
8661075
];
8671076
want.sort();
8681077
assert_eq!(got, want);
869-
assert!(with_store_variants(Vec::new()).await.is_empty());
1078+
assert!(with_store_peer_variant_copies(Vec::new()).await.is_empty());
8701079
}
8711080

8721081
/// vlt twin of the `.pnpm` case: every importer entry is a link into

‎crates/socket-patch-cli/src/ecosystem_dispatch.rs‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::path::PathBuf;
77

88
use crate::args::GlobalArgs;
99

10+
use socket_patch_core::crawlers::npm_crawler::with_store_peer_variant_copies;
1011
use socket_patch_core::crawlers::walk_pool;
1112
use socket_patch_core::crawlers::CargoCrawler;
1213
use socket_patch_core::crawlers::ComposerCrawler;
@@ -593,8 +594,8 @@ pub(crate) fn npm_paths_by_identity_in(
593594
/// patch would silently resolve as `package_not_found`. The rollback
594595
/// variant fans each base path back out to every qualified manifest PURL
595596
/// — the same mapping the manifest was written with (`get` uses the same
596-
/// resolver). `vex` hashes the first copy of a manifest purl and every copy
597-
/// of a hosted one from this one lookup.
597+
/// resolver). `vex` hashes every copy of a manifest purl and of a hosted
598+
/// one from this one lookup; npm copies include their store variants.
598599
///
599600
/// With `prior`, the npm `node_modules` roots come
600601
/// from it (a crawl of the same options earlier in this process, over
@@ -612,14 +613,23 @@ pub async fn find_manifest_package_copies_reusing(
612613
let partitioned = partition_purls(purls, common.ecosystems.as_deref());
613614
let crawler_options = common.crawler_options();
614615
let npm_roots = prior.and_then(|p| p.roots_for(&crawler_options));
615-
dispatch_find(
616+
let mut copies = dispatch_find(
616617
&partitioned,
617618
&crawler_options,
618619
quiet,
619620
merge_qualified,
620621
npm_roots,
621622
)
622-
.await
623+
.await;
624+
// `apply` also writes every store variant of each npm copy (a pnpm peer
625+
// suffix, a Deno `_N` copy index, a vlt peer extra), so "every copy"
626+
// includes them (#603).
627+
for (purl, paths) in copies.iter_mut() {
628+
if purl.starts_with("pkg:npm/") {
629+
*paths = with_store_peer_variant_copies(std::mem::take(paths)).await;
630+
}
631+
}
632+
copies
623633
}
624634

625635
/// Box the future `make` returns, constructing it inside this (non-async)

0 commit comments

Comments
 (0)