Skip to content

Commit aeddea4

Browse files
mikolalysenkoclaude
andcommitted
Merge origin/main into arch-fix/vex-false-attest
Resolve conflicts in vex/discover: keep main's same-lock contest (#940) alongside this branch's unwired-copy unattestation, pnpm bundled copies and berry registry-locator contest. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2 parents e9a33b8 + 431b818 commit aeddea4

53 files changed

Lines changed: 4765 additions & 515 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/ci.yml‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,19 @@ jobs:
800800
- name: Run npm dispatch tests
801801
run: node --test npm/socket-patch/bin/socket-patch.test.mjs
802802

803+
- name: Run npm schema tests
804+
# The `./schema` export's zod tests. `npm install --no-save`, not
805+
# `npm ci`, for the reason publish-npm.yml gives (a version-synced
806+
# lock can name platform packages not yet on the registry); the
807+
# platform binaries are optional and not needed here. The pack
808+
# test runs again once `npm test` has built dist/, so it also
809+
# checks that the compiled schema ships.
810+
working-directory: npm/socket-patch
811+
run: |
812+
npm install --no-save --ignore-scripts --no-audit --no-fund --omit=optional
813+
npm test
814+
node --test --test-name-pattern="npm package contents" bin/socket-patch.test.mjs
815+
803816
# Compiles the CLI and every CLI test target once per OS (--all-features,
804817
# so this is also the feature-gated suites' compile-rot check) and uploads
805818
# the binaries the e2e, e2e-full and cargo-vex legs run. The legs run the

‎crates/socket-patch-bench/src/fixtures/npm.rs‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -537,7 +537,17 @@ pub fn yarn_classic_lock(g: &Graph) -> String {
537537

538538
pub fn build_yarn_classic(t: &mut Tree, size: Size) -> std::io::Result<Fixture> {
539539
let g = graph("yarn-classic", size);
540-
t.write("project/package.json", g.package_json())?;
540+
// Pin yarn 1 as a real classic project does: without the pin a hosted
541+
// scan rightly warns `redirect_yarn_classic_berry_migration_risk`
542+
// (#907), which the scenario would count as an unexpected warning.
543+
t.write(
544+
"project/package.json",
545+
g.package_json().replacen(
546+
"\"private\": true",
547+
"\"private\": true,\n \"packageManager\": \"yarn@1.22.22\"",
548+
1,
549+
),
550+
)?;
541551
t.write("project/yarn.lock", yarn_classic_lock(&g))?;
542552
t.write(
543553
"project/node_modules/.yarn-integrity",

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

Lines changed: 14 additions & 10 deletions
Large diffs are not rendered by default.

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

Lines changed: 53 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1743,7 +1743,11 @@ type LockRefusals = HashMap<String, (&'static str, String)>;
17431743
/// refusal and the ledger's idempotency skip, which take precedence in the
17441744
/// fetch loop. A purl the lockfiles pin hosted is left to the vendor loop:
17451745
/// its takeover restores the upstream lock entry first, and the restore
1746-
/// rewrites the very text the gates read.
1746+
/// rewrites the very text the gates read. The one exception is a hosted
1747+
/// gem the takeover would refuse ([`gem_takeover_refusals_for`]), which
1748+
/// is refused here with the takeover's code instead of after its fetch.
1749+
///
1750+
/// [`gem_takeover_refusals_for`]: crate::commands::vendor::gem_takeover_refusals_for
17471751
///
17481752
/// Only a package the vendor loop would hand to its backend is refused
17491753
/// here (see [`crate::commands::vendor::lock_refusals_reaching_backend`]):
@@ -1760,44 +1764,65 @@ async fn lock_text_refusals_for(
17601764
prior: Option<&crate::ecosystem_dispatch::NpmCrawlSnapshot>,
17611765
) -> LockRefusals {
17621766
let cwd = params.cwd.as_path();
1763-
let claimed: Vec<String> = socket_patch_core::patch::redirect::upstream::HostedPin::all(
1767+
let origins: Vec<String> = params
1768+
.patch_server_url
1769+
.iter()
1770+
.filter(|url| !url.trim().is_empty())
1771+
.cloned()
1772+
.collect();
1773+
let pins = socket_patch_core::patch::redirect::upstream::HostedPin::all(
17641774
&socket_patch_core::vex::discover_patched_refs_with(
17651775
cwd,
17661776
&socket_patch_core::vex::DiscoverOptions {
1767-
patch_server_origins: params
1768-
.patch_server_url
1769-
.iter()
1770-
.filter(|url| !url.trim().is_empty())
1771-
.cloned()
1772-
.collect(),
1777+
patch_server_origins: origins.clone(),
17731778
},
17741779
)
17751780
.await,
1776-
)
1777-
.into_iter()
1778-
.map(|pin| canonical_purl(&pin.purl))
1779-
.collect();
1780-
let candidates: Vec<(&str, &str)> = selected
1781+
);
1782+
let claimed: Vec<String> = pins.iter().map(|pin| canonical_purl(&pin.purl)).collect();
1783+
let fetchable: Vec<&PatchSearchResult> = selected
17811784
.iter()
17821785
.filter(|sr| bun_refusal.filter(|r| r.applies_to(&sr.purl)).is_none())
17831786
.filter(|sr| {
17841787
detached_ledger_record(RecordStore::Ledger(&ledger.entries), &sr.purl, &sr.uuid)
17851788
.is_none()
17861789
})
1790+
.collect();
1791+
let candidates: Vec<(&str, &str)> = fetchable
1792+
.iter()
17871793
.filter(|sr| !claimed.contains(&canonical_purl(&sr.purl)))
17881794
.map(|sr| (sr.purl.as_str(), sr.uuid.as_str()))
17891795
.collect();
17901796
let refused = socket_patch_core::vendor::lock_text_refusals(cwd, &candidates).await;
17911797
let options = params.crawler_options();
1792-
crate::commands::vendor::lock_refusals_reaching_backend(
1793-
cwd,
1794-
refused,
1795-
&ledger.entries,
1796-
|purls| async move {
1797-
crate::commands::vendor::installed_purls(&options, &purls, prior).await
1798-
},
1799-
)
1800-
.await
1798+
let mut refusals =
1799+
crate::commands::vendor::lock_refusals_reaching_backend(
1800+
cwd,
1801+
refused,
1802+
&ledger.entries,
1803+
|purls| async move {
1804+
crate::commands::vendor::installed_purls(&options, &purls, prior).await
1805+
},
1806+
)
1807+
.await;
1808+
// A hosted gem the takeover will refuse (#775) is refused here too, so
1809+
// its view is never fetched for a package the run cannot vendor. The
1810+
// download phase only runs online (`--offline` refuses `get` and `scan`
1811+
// before it), so the dry-run restore may resolve the registry entry.
1812+
refusals.extend(
1813+
crate::commands::vendor::gem_takeover_refusals_for(
1814+
cwd,
1815+
fetchable
1816+
.iter()
1817+
.filter(|sr| claimed.contains(&canonical_purl(&sr.purl)))
1818+
.map(|sr| sr.purl.as_str()),
1819+
&pins,
1820+
false,
1821+
origins,
1822+
)
1823+
.await,
1824+
);
1825+
refusals
18011826
}
18021827

18031828
/// The record a detached ledger entry already carries for `purl` at
@@ -3759,7 +3784,12 @@ async fn run_get_vendored(
37593784
// Dry run: ledger-classification preview only (scan's posture) — no
37603785
// download, no vendor step, no writes.
37613786
if args.common.dry_run {
3762-
let preview = super::scan::preview_vendor_json(&args.common.cwd, selected).await;
3787+
let takeover = super::vendor::gem_takeover_preview_refusals(
3788+
&args.common,
3789+
selected.iter().map(|p| p.purl.as_str()),
3790+
)
3791+
.await;
3792+
let preview = super::scan::preview_vendor_json(&args.common.cwd, selected, &takeover).await;
37633793
if args.common.json {
37643794
let mut result = serde_json::json!({
37653795
"status": "success",

‎crates/socket-patch-cli/src/commands/scan/hosted.rs‎

Lines changed: 43 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,9 @@ fn refuse(
137137
}
138138

139139
/// The apply lock for a WET hosted run: the same `<manifest dir>/apply.lock`
140-
/// `apply`/`rollback`/`remove`/`vendor` hold, so the takeover pre-reverts,
141-
/// the ledger merge and the lockfile writes never race them. `acquire`
140+
/// `apply`/`rollback`/`remove`/`vendor` hold, so the takeover pre-reverts
141+
/// (lockfiles + the vendored ledger) and the lockfile writes never race
142+
/// them. `acquire`
142143
/// creates a missing `.socket/` and the guard's drop unlinks the lock file
143144
/// and prunes an otherwise-empty `.socket/`, so a run that ends up writing
144145
/// nothing leaves no residue. Contention / IO failures render through the
@@ -299,11 +300,10 @@ async fn installed_stale_positive_evidence(
299300
/// refuses as a write root is still READ here
300301
/// (`verification_only_gem_paths`): bundler installs into it.
301302
/// * Records are found BY UUID (the fetch key, stable across purl
302-
/// spellings): this run's fetched records first, then the redirect
303-
/// ledger's persisted ones — a re-scan whose `/patches/view` fetch failed
304-
/// transiently still re-fires from the ledger instead of silently
305-
/// dropping the warning (`record_fetch_failed` covers the fetch failure
306-
/// itself). Record availability is part of the candidate filter, and the
303+
/// spellings) among this run's fetched records; v5 keeps no hosted
304+
/// ledger to fall back on, so a uuid whose `/patches/view` fetch failed
305+
/// is not judged (`record_fetch_failed` surfaces that failure). Record
306+
/// availability is part of the candidate filter, and the
307307
/// probe returns before any crawler work (or `gem env` subprocess spawn)
308308
/// when no judgment is possible.
309309
/// * PATCHED means [`verify_patch_record`] `Ok` — the one shared oracle
@@ -326,8 +326,7 @@ async fn gem_stale_install_warnings(
326326
global: bool,
327327
global_prefix: Option<std::path::PathBuf>,
328328
confirmed: &[(String, String)],
329-
// This run's fetched records MERGED with the ledger's persisted ones
330-
// (the caller hands the post-merge ledger map).
329+
// This run's fetched records, by uuid.
331330
records: &std::collections::BTreeMap<String, socket_patch_core::manifest::schema::PatchRecord>,
332331
gem_artifact_shas: &std::collections::BTreeMap<(String, String), String>,
333332
) -> StaleInstallOutcome {
@@ -629,20 +628,20 @@ pub(super) async fn run_redirect(
629628
/// ([`socket_patch_core::hosted::engine`], over a
630629
/// [`ProjectView::Disk`](socket_patch_core::vendor::lock_inventory::ProjectView));
631630
/// what stays here is what needs the host: reference grants and the other
632-
/// network fetches, the apply lock (wet runs with a grant), the redirect
633-
/// ledger load, the vendored→hosted takeover pre-revert (symlink-checked
634-
/// first), the `pipenv --version` probe, the symlink guard, the ledger
635-
/// merge-then-persist and the file writes, the gem / Python / vlt
631+
/// network fetches, the apply lock (wet runs with a grant), the
632+
/// vendored→hosted takeover pre-revert (symlink-checked first), the
633+
/// `pipenv --version` probe, the symlink guard, the file writes, the gem /
634+
/// Python / vlt
636635
/// stale-install probes, and the optional VEX. Shared VERBATIM by `scan
637636
/// --mode hosted` (its `--json` arm through the `run_redirect` wrapper, its
638637
/// human arm through [`boxed_run_redirect_selected`] in `scan/mod.rs`; both
639638
/// select via `discover_selected`, with no prompt) and by `get --mode
640639
/// hosted` (which pins the advisory-resolved uuid), so all produce
641-
/// identical on-disk results for the same selection. The redirect ledger
642-
/// is loaded HERE, under the apply lock whenever this run holds one (never
643-
/// handed in pre-loaded: a copy read before the lock could merge over a
644-
/// concurrent writer's edits); a dry run or a zero-grant run reads it
645-
/// strictly but writes nothing, quarantine included.
640+
/// identical on-disk results for the same selection. v5 hosted mode keeps
641+
/// no ledger (the lockfiles are the only record); the VENDORED ledger the
642+
/// takeover needs is loaded HERE, under the apply lock whenever this run
643+
/// holds one (never handed in pre-loaded: a copy read before the lock could
644+
/// be saved over a concurrent writer's edits).
646645
///
647646
/// `scan_result` must be `Some` exactly when `common.json` is set (the
648647
/// human/JSON split keys on `common.json`; a `--json` caller passing `None`
@@ -1091,10 +1090,10 @@ pub(crate) async fn run_redirect_selected(
10911090
std::collections::BTreeMap::new();
10921091
let mut record_warnings: Vec<socket_patch_core::patch::redirect::RewriteWarning> = Vec::new();
10931092

1094-
// SYMLINK GUARD (see `engine::guard`) — before the ledger and before any
1095-
// write, dry runs included, so a dry run predicts the refusal. The
1096-
// revert side (replay.rs) already refuses linked files, so the write
1097-
// side must too.
1093+
// SYMLINK GUARD (see `engine::guard`) — before any write, dry runs
1094+
// included, so a dry run predicts the refusal. The revert side (the
1095+
// hosted → upstream restore's staged flush) already refuses linked
1096+
// files, so the write side must too.
10981097
if let Some(refusal) = engine::guard(&view, &done, &candidates) {
10991098
return refuse(common, scan_result.take(), &refusal);
11001099
}
@@ -1176,8 +1175,8 @@ pub(crate) async fn run_redirect_selected(
11761175
// the writes so the warning describes the project as this run leaves it.
11771176
// Idempotent re-scans re-confirm and re-probe, so the warning keeps
11781177
// firing until the stale materialization is actually gone. Skipped
1179-
// EXPLICITLY on --dry-run: the probe's ledger-record fallback would
1180-
// otherwise judge state the run did not (re)create.
1178+
// EXPLICITLY on --dry-run: nothing was written, so the probe would
1179+
// judge state the run did not (re)create.
11811180
let gem_stale: StaleInstallOutcome = if common.dry_run {
11821181
StaleInstallOutcome::default()
11831182
} else {
@@ -1330,7 +1329,7 @@ pub(crate) async fn run_redirect_selected(
13301329
// Stale-flagged purls are EXCLUDED from assume_applied: the same-run
13311330
// envelope carries a redirect_gem_stale_install warning proving the
13321331
// installed materialization unpatched, so attesting that purl from
1333-
// the ledger would contradict the run's own warning. Excluded purls
1332+
// this run's records would contradict the run's own warning. Excluded purls
13341333
// fall back to `vex`'s normal installed-tree verification.
13351334
// A confirmed uuid whose bundled instance the rewriter had to skip
13361335
// (#469) leaves that copy unpatched, so it too is verified, never
@@ -1745,8 +1744,8 @@ async fn vendored_takeover(
17451744
None
17461745
};
17471746
// Yarn berry twin of the bun gate: the berry rewriter's project-level
1748-
// refusals (mixed line endings, cacheKey, `.yarnrc.yml`
1749-
// compressionLevel) must be known before the takeover reverts a
1747+
// refusals (mixed yarn.lock / package.json line endings, cacheKey,
1748+
// `.yarnrc.yml` compressionLevel) must be known before the takeover reverts a
17501749
// vendored berry purl, or the revert strips the live vendored patch
17511750
// and the rewriter then refuses the lock. Only entries the
17521751
// vendor ledger wired through the yarn-berry backend are gated (the
@@ -1768,8 +1767,14 @@ async fn vendored_takeover(
17681767
)
17691768
.await
17701769
.ok();
1770+
let manifest = socket_patch_core::utils::fs::read_regular_to_string(
1771+
&common.cwd.join("package.json"),
1772+
)
1773+
.await
1774+
.ok();
17711775
socket_patch_core::patch::redirect::preflight_yarn_berry_hosted(
17721776
&lock,
1777+
manifest.as_deref(),
17731778
yarnrc.as_deref(),
17741779
)
17751780
.err()
@@ -3382,8 +3387,8 @@ mod tests {
33823387

33833388
/// Probe invocation with the default surface (project-local discovery,
33843389
/// no artifact shas) — tests override the knobs they exercise.
3385-
/// `records` is the merged map production hands over (this run's
3386-
/// fetched records plus the ledger's persisted ones).
3390+
/// `records` is the map production hands over: this run's fetched
3391+
/// records, by uuid.
33873392
async fn probe(
33883393
cwd: &std::path::Path,
33893394
confirmed: &[(String, String)],
@@ -3712,25 +3717,22 @@ mod tests {
37123717
);
37133718
}
37143719

3715-
/// RE-FIRE guarantee: when this run's record fetch failed (no fresh
3716-
/// records), the merged map the caller hands over still carries the
3717-
/// redirect ledger's PERSISTED record under whatever purl key the
3718-
/// ledger used — and the probe's uuid lookup judges from it, so a
3719-
/// transient /patches/view failure cannot silently retire the warning
3720-
/// while the stale materialization is still there.
3720+
/// The probe links a record to a confirmed purl by uuid alone: a
3721+
/// record keyed under the API's qualified purl spelling (not the
3722+
/// confirmed purl) must still judge the stale materialization.
37213723
#[tokio::test]
3722-
async fn gem_stale_probe_judges_from_persisted_ledger_records() {
3724+
async fn gem_stale_probe_matches_records_by_uuid_not_purl_key() {
37233725
let stale = tempfile::tempdir().unwrap();
37243726
materialize_gem(stale.path(), GEM_UPSTREAM);
3725-
// Persisted under the API's qualified spelling, not the confirmed
3727+
// Keyed under the API's qualified spelling, not the confirmed
37263728
// purl: only the uuid links them.
3727-
let mut ledger_only = std::collections::BTreeMap::new();
3728-
ledger_only.insert(format!("{GEM_PURL}?platform=ruby"), gem_record());
3729-
let out = probe(stale.path(), &one_confirmed(), &ledger_only).await;
3729+
let mut qualified = std::collections::BTreeMap::new();
3730+
qualified.insert(format!("{GEM_PURL}?platform=ruby"), gem_record());
3731+
let out = probe(stale.path(), &one_confirmed(), &qualified).await;
37303732
assert_eq!(
37313733
out.warnings.len(),
37323734
1,
3733-
"the ledger records must keep the warning firing across flaky fetches"
3735+
"a record keyed under another purl spelling must still match by uuid"
37343736
);
37353737
}
37363738

0 commit comments

Comments
 (0)