Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/socket-patch-cli/CLI_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ For a **9.0 root lock**, the CLI ensures `pnpm-workspace.yaml` carries `trustLoc

**vlt hosted-mode contract**: `scan` / `get --mode hosted` rewrite, in `vlt-lock.json`, every default-registry node of a granted `name@version` (the `''` / `npm` segment or a URL segment equal to the lock's scalar `registry`, both DepID grammars, every peer and modifier variant): slot [2] becomes the granted sha512 and slot [3] the hosted URL (appended to a 3-tuple); the DepID, flags and trailing slots, the line ending and every other byte stay. `options` is never edited and `vlt.json` is only read. A lock with another `lockfileVersion` (decided on the raw JSON token), a BOM, a non-object body or a `nodes` section outside vlt's one-node-per-line layout refuses the whole lock (`redirect_vlt_lock_unsupported`). **Confirmation**: vlt drives when its install state (`node_modules/.vlt-lock.json` or `node_modules/.vlt/`) is present or no other npm-family lock is; then only `vlt-lock.json` confirms a uuid. Otherwise every lock is rewritten, `redirect_vlt_sibling_lockfiles` warns, and the other locks' rules confirm, including a dep `vlt-lock.json` merely does not wire (`redirect_vlt_entry_not_found`, `redirect_vlt_entry_vendored`). Whichever lock drives, a dep the vlt rewriter refuses (`redirect_vlt_missing_sha512`, `redirect_vlt_unsupported_lock_key`) is never confirmed by any lock, although a sibling lock may already carry its rewritten URL. **Artifact preflight**: before any takeover or write (dry runs included), each granted artifact with a default-registry instance is fetched once as vlt fetches it and must verify, else the dep is withheld (`redirect_vlt_artifact_unverifiable`, see the tag table). **Heal**: stale installed copies of Socket-owned nodes are removed so the next `vlt install` extracts the patched bytes, and `rollback` / `remove` do the same for the registry bytes (`--no-vlt-install-cleanup` keeps them; optional dependencies' copies are always kept); `redirect_vlt_reinstall_required` says what happened and what to run. The same-run `--vex` never attests a vlt package whose installed copy is stale or unchecked, whose lock a vlt release may ignore (`redirect_vlt_lockfile_version_missing`, `redirect_vlt_old_lockfile_ignored`, `redirect_vlt_scalar_registry_ignored`), or which also resolves from a non-default registry (`redirect_vlt_custom_registry_skipped`). `vlt.json` or vlt install state without `vlt-lock.json` warns `redirect_vlt_no_lockfile` instead of `redirect_npm_no_lockfile`. `rollback` / `remove` restore each hosted node's slots [2] and [3] from the npm registry, following the lock's own slot-[3] convention (see "Hosted unwind coverage"). Tested releases: `docs/testing/vlt-compatibility.md`.

**Takeover reconciliation (every hosted ecosystem, v5.0)**: vendoring over a hosted pin (`vendor`, `scan --mode vendored`, `get --mode vendored`) first RESTORES that purl's lock entries to their default upstream registry entry — the same restore `rollback` runs (core `patch::redirect::upstream::restore_upstream`; see "Hosted unwind coverage"), over the hosted pins lockfile discovery finds (v5 keeps no hosted ledger) — and then vendors, so the vendor ledger records the PRISTINE registry entry as its wiring `original` and `vendor --revert` lands back on upstream registry state, never on hosted. The run that takes over records a `vendor_takeover_reverted_redirect` advisory event (`skipped` action beside the purl's genuine outcome; detail `<purl> was hosted; restored its upstream registry entry (<files>) before vendoring (mode takeover)`; the human path prints `Warning: …`), plus any advisory the restore raised (`npm_allow_remote_left`, …). `--dry-run` resolves the same restore without writing (registry lookups included): a pin that would restore reports `vendor_would_revert_redirect`, and one that would be refused surfaces in the preview with the wet run's `redirect_revert_failed` code and detail (for bun, whose hosted rewrite replaces the entry's `name@version` spec, the preview first runs the Bun vendored preflight described below and then stops at the advisory instead of reading the still-hosted lock — a lock the vendored backend would refuse is previewed as the wet run's `failed <code>`, never as `vendor_would_revert_redirect`). A purl whose upstream entry cannot be restored — `--offline`, a registry that does not answer, a lock the restore refuses (see "Hosted unwind coverage"; a hosted binary `bun.lockb` pin IS restored for the takeover — its npm registry record is rebuilt natively — while `rollback` / `remove` refuse it) — fails `redirect_revert_failed` with the detail `cannot vendor over the live hosted pin: cannot restore <purl> to its upstream registry entry: <why>; restore it from version control instead (`git checkout -- <files>`)` (exit 1 / `partial_failure`, nothing vendored for it, the hosted wiring left in place). The cargo backend's `hosted_redirect_live` refusal backstops a crate whose hosted residue is still in place when it is reached; its detail names `socket-patch rollback` and `git checkout -- Cargo.toml Cargo.lock`. **Bun vendored preflight before the takeover**: `vendor` — like `scan` / `get --mode vendored`, whose pre-download preflight runs earlier — checks `bun.lock` / `bun.lockb` with the shared Bun vendored preflight BEFORE the upstream restore, so a hosted purl on a lock the vendored backend refuses (a pre-version-2 `workspace:` lock → `vendor_bun_workspace_unsupported`; a malformed or unsupported binary lock → `vendor_bun_lockb_invalid`; an unsupported text-lock version → its code) is reported `failed <code>` with the hosted wiring and active Bun lock byte-untouched (exit 1 / `partial_failure`): the package stays hosted-patched instead of being un-hosted and then refused. `vendor --dry-run` previews that same `failed` code (exit-code parity with the wet run, nothing written) instead of promising `vendor_would_revert_redirect`. Pinned by `tests/in_process_vendor_bun_takeover.rs` and, against real Bun, `tests/mode_migration_bun.rs`. Hosted → vendored and vendored → hosted (`redirect_takeover_reverted_vendored` in `redirect.warnings[]`) both work in place on the locks the target mode accepts. **Removed in v5.0**: the run-level `vendor_supersedes_redirect` warning and its reconcile of the redirect ledger (a live lock that already proved vendored won over a stale hosted ledger record) — once the lock routes a package to `.socket/vendor/`, no hosted state is left to go stale. Which way the live lock points is decided by the same lockfile discovery rules `vex` gates attestations on (see "Manifest-less VEX (lockfile discovery)"), for `redirect_supersedes_vendored` and `hosted_wiring_retained` alike.
**Takeover reconciliation (every hosted ecosystem, v5.0)**: vendoring over a hosted pin (`vendor`, `scan --mode vendored`, `get --mode vendored`) first RESTORES that purl's lock entries to their default upstream registry entry — the same restore `rollback` runs (core `patch::redirect::upstream::restore_upstream`; see "Hosted unwind coverage"), over the hosted pins lockfile discovery finds (v5 keeps no hosted ledger) — and then vendors, so the vendor ledger records the PRISTINE registry entry as its wiring `original` and `vendor --revert` lands back on upstream registry state, never on hosted. The run that takes over records a `vendor_takeover_reverted_redirect` advisory event (`skipped` action beside the purl's genuine outcome; detail `<purl> was hosted; restored its upstream registry entry (<files>) before vendoring (mode takeover)`; the human path prints `Warning: …`), plus any advisory the restore raised (`npm_allow_remote_left`, …). `--dry-run` resolves the same restore without writing (registry lookups included): a pin that would restore reports `vendor_would_revert_redirect`, and one that would be refused surfaces in the preview with the wet run's `redirect_revert_failed` code and detail (for bun, whose hosted rewrite replaces the entry's `name@version` spec, the preview first runs the Bun vendored preflight described below and then stops at the advisory instead of reading the still-hosted lock — a lock the vendored backend would refuse is previewed as the wet run's `failed <code>`, never as `vendor_would_revert_redirect`). A purl whose upstream entry cannot be restored — `--offline`, a registry that does not answer, a lock the restore refuses (see "Hosted unwind coverage"; a hosted binary `bun.lockb` pin IS restored for the takeover — its npm registry record is rebuilt natively — while `rollback` / `remove` refuse it) — fails `redirect_revert_failed` with the detail `cannot vendor over the live hosted pin: cannot restore <purl> to its upstream registry entry: <why>; restore it from version control instead (`git checkout -- <files>`)` (exit 1 / `partial_failure`, nothing vendored for it, the hosted wiring left in place). The cargo backend's `hosted_redirect_live` refusal backstops a crate whose hosted residue is still in place when it is reached; its detail names `socket-patch rollback` and `git checkout -- Cargo.toml Cargo.lock`. **Bun vendored preflight before the takeover**: `vendor` — like `scan` / `get --mode vendored`, whose pre-download preflight runs earlier — checks `bun.lock` / `bun.lockb` with the shared Bun vendored preflight BEFORE the upstream restore, so a hosted purl on a lock the vendored backend refuses (a pre-version-2 `workspace:` lock → `vendor_bun_workspace_unsupported`; a malformed or unsupported binary lock → `vendor_bun_lockb_invalid`; an unsupported text-lock version → its code) is reported `failed <code>` with the hosted wiring and active Bun lock byte-untouched (exit 1 / `partial_failure`): the package stays hosted-patched instead of being un-hosted and then refused. `vendor --dry-run` previews that same `failed` code (exit-code parity with the wet run, nothing written) instead of promising `vendor_would_revert_redirect`. Pinned by `tests/in_process_vendor_bun_takeover.rs` and, against real Bun, `tests/mode_migration_bun.rs`. The npm package-lock backend's lock gate gets the same placement: a hosted pin in a project whose `npm-shrinkwrap.json` / `package-lock.json` is not a v2/v3 lock (npm 6's lockfileVersion 1) is refused `failed vendor_lockfile_version_unsupported` BEFORE the restore, in `vendor`, `scan --mode vendored` and `get --mode vendored` alike, so the package stays hosted-patched; the vendored dry-run preview lists every npm purl of such a project as `would_refuse` with that code. Pinned by `tests/in_process_vendor_npm_v1_takeover.rs`. Hosted → vendored and vendored → hosted (`redirect_takeover_reverted_vendored` in `redirect.warnings[]`) both work in place on the locks the target mode accepts. **Removed in v5.0**: the run-level `vendor_supersedes_redirect` warning and its reconcile of the redirect ledger (a live lock that already proved vendored won over a stale hosted ledger record) — once the lock routes a package to `.socket/vendor/`, no hosted state is left to go stale. Which way the live lock points is decided by the same lockfile discovery rules `vex` gates attestations on (see "Manifest-less VEX (lockfile discovery)"), for `redirect_supersedes_vendored` and `hosted_wiring_retained` alike.

### Scan modes (v5.0)

Expand Down
32 changes: 29 additions & 3 deletions crates/socket-patch-cli/src/commands/scan/vendor_flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,11 @@ type VendorStepResult = Result<(bool, Envelope), VendorStepError>;
/// Action values are part of the CLI contract: `would_vendor` (no ledger
/// entry), `already_vendored` (entry at this uuid), `would_revendor` +
/// `oldUuid` (entry at an older uuid), and — additive — `would_refuse` +
/// `errorCode` + `error` for npm purls the wet run's Bun or vlt preflight
/// `errorCode` + `error` for npm purls the wet run's Bun, vlt or npm
/// package-lock preflight
/// ([`crate::commands::bun_preflight::BunVendorRefusal`],
/// [`crate::commands::vlt_preflight`]) would refuse before any download.
/// [`crate::commands::vlt_preflight`], [`npm_lock_refusal`]) would refuse
/// before any download.
/// The preview stays a ledger classification otherwise (engine refusals
/// outside the preflights are not predicted), and `would_refuse` never
/// flips the run's status or exit code. The preflights (the only disk
Expand All @@ -88,6 +90,7 @@ pub(crate) async fn preview_vendor_json(
bun_vendor_preflight_with_ledger(cwd, selected, state.as_ref().map(|s| &s.entries)).await;
let vlt_refusals =
vlt_vendor_preflight_selected(cwd, selected, state.as_ref().map(|s| &s.entries)).await;
let npm_lock_refusal = npm_lock_refusal(cwd, selected).await;
let state = state.unwrap_or_default();
let mut patches: Vec<serde_json::Value> = selected
.iter()
Expand All @@ -108,6 +111,13 @@ pub(crate) async fn preview_vendor_json(
"errorCode": r.code, "error": r.detail,
})
}
_ if p.purl.starts_with("pkg:npm/") && npm_lock_refusal.is_some() => {
let (code, detail) = npm_lock_refusal.as_ref().expect("checked by the guard");
serde_json::json!({
"purl": p.purl, "uuid": p.uuid, "action": "would_refuse",
"errorCode": code, "error": detail,
})
}
Some(e) if e.uuid == p.uuid => serde_json::json!({
"purl": p.purl, "uuid": p.uuid, "action": "already_vendored",
}),
Expand All @@ -124,7 +134,7 @@ pub(crate) async fn preview_vendor_json(
serde_json::json!({ "dryRun": true, "patches": patches })
}

/// The purls of `selected` the wet run's Bun or vlt preflight would refuse
/// The purls of `selected` the wet run's Bun, vlt or npm package-lock preflight would refuse
/// before any download (the `would_refuse` rows of
/// [`preview_vendor_json`]): the vendored planning pass, so a refused NEW
/// patch holds no rollout slot.
Expand All @@ -137,16 +147,32 @@ pub(super) async fn preflight_refused_purls(
bun_vendor_preflight_with_ledger(cwd, selected, state.as_ref().map(|s| &s.entries)).await;
let vlt_refusals =
vlt_vendor_preflight_selected(cwd, selected, state.as_ref().map(|s| &s.entries)).await;
let npm_lock_refusal = npm_lock_refusal(cwd, selected).await;
selected
.iter()
.filter(|p| {
refusal.as_ref().is_some_and(|r| r.applies_to(&p.purl))
|| vlt_refusal_for(&vlt_refusals, &p.purl).is_some()
|| (p.purl.starts_with("pkg:npm/") && npm_lock_refusal.is_some())
})
.map(|p| p.purl.clone())
.collect()
}

/// The npm package-lock backend's project-level refusal (a lock that is
/// not v2/v3, see [`socket_patch_core::vendor::npm_lock_vendor_preflight`]),
/// which refuses every npm purl of the project before any download or
/// takeover. Read only when the selection holds an npm purl.
async fn npm_lock_refusal(
cwd: &Path,
selected: &[PatchSearchResult],
) -> Option<(&'static str, String)> {
if !selected.iter().any(|p| p.purl.starts_with("pkg:npm/")) {
return None;
}
socket_patch_core::vendor::npm_lock_vendor_preflight(cwd).await
}

/// Human rendering of the vendored dry-run preview's `would_refuse` records
/// (see [`preview_vendor_json`]): the count line above it still says
/// "would download and vendor", so name what the wet run would refuse and
Expand Down
30 changes: 22 additions & 8 deletions crates/socket-patch-cli/src/commands/vendor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2207,14 +2207,16 @@ pub(crate) async fn vendor_records_reusing(
.find(|pin| canonical_purl(&pin.purl) == canonical_purl(purl))
};

// Yarn berry takeover preflight (see
// `socket_patch_core::vendor::yarn_berry_vendor_preflight`): the berry
// backend's project-level refusals (mixed line endings in yarn.lock or
// package.json, cacheKey, `.yarnrc.yml` compressionLevel), computed at
// Yarn berry / npm package-lock takeover preflight (see
// `socket_patch_core::vendor::yarn_berry_vendor_preflight` and
// `npm_lock_vendor_preflight`): the backend's project-level refusals
// (berry: mixed line endings in yarn.lock or package.json, cacheKey,
// `.yarnrc.yml` compressionLevel; package-lock: a lock that is not
// v2/v3, #659), computed at
// most once per run and only when a hosted-claimed npm purl reaches the
// takeover below, which must refuse such a purl BEFORE reverting its
// hosted edits.
let berry_takeover_refusal: tokio::sync::OnceCell<Option<(&'static str, String)>> =
let npm_takeover_refusal: tokio::sync::OnceCell<Option<(&'static str, String)>> =
tokio::sync::OnceCell::new();
let pipenv_version = tokio::sync::OnceCell::new();
// The vlt store entries each hosted→vendored takeover unpinned, healed
Expand Down Expand Up @@ -2410,9 +2412,21 @@ pub(crate) async fn vendor_records_reusing(
}
}
if candidate.starts_with("pkg:npm/") {
let project = berry_takeover_refusal
.get_or_init(|| {
socket_patch_core::vendor::yarn_berry_vendor_preflight(&common.cwd)
let project = npm_takeover_refusal
.get_or_init(|| async {
match socket_patch_core::vendor::yarn_berry_vendor_preflight(
&common.cwd,
)
.await
{
Some(refusal) => Some(refusal),
None => {
socket_patch_core::vendor::npm_lock_vendor_preflight(
&common.cwd,
)
.await
}
}
})
.await
.clone();
Expand Down
Loading
Loading