From e9d8e31293f9937dd3b9d2fe2232466e22394ab9 Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Wed, 4 Nov 2026 11:23:41 -0600 Subject: [PATCH] Stop downloading vlt artifacts during hosted scans A hosted scan of a vlt project downloaded every granted patch archive the way vlt fetches it, and withheld the dependency (`redirect_vlt_artifact_unverifiable`) when the server re-gzipped it. That guarded against patch.socket.dev's CDN re-encoding archives, which vlt rejects with EINTEGRITY. The patch server has sent archives with `Cache-Control: no-transform` since depscan #26856 and #26867, and production serves them unencoded today. On any mirror that still re-encodes, `vlt ci` fails closed with EINTEGRITY, so nothing installs unpatched bytes. So the scan no longer fetches the archives. It pins vlt dependencies directly, without a full download of every patched package per run. The in-memory hosted engine (the Node addon and `hosted-bundle`) has no network, so it used to withhold every vlt dependency as "offline"; it now pins them the way the disk flow does. The warm-tree heal now checks the Socket-owned lock instances this run confirmed in `vlt-lock.json`. With no patch record and no hidden lock it reports the copy as undeterminable (asking for `vlt ci`) instead of comparing it against downloaded archive bytes. The vlt backtest still probes the live artifact itself: a must-patch hosted cell records `blocked-by-server-encoding` when its probe sees a content-encoded response, without running the CLI. Assisted-by: Claude Code:claude-opus-5-5 --- .github/workflows/ci.yml | 3 +- .github/workflows/vlt-compatibility.yml | 8 +- .github/workflows/vlt-serve-watchdog.yml | 4 +- crates/socket-patch-cli/CLI_CONTRACT.md | 5 +- .../src/commands/scan/hosted.rs | 54 +- .../socket-patch-cli/src/commands/vlt_heal.rs | 210 +------ .../tests/e2e_hosted_production.rs | 90 +-- .../tests/e2e_redirect_vlt_build.rs | 45 +- .../tests/e2e_vex_lockfile/vlt.rs | 6 +- .../tests/hosted_memory_engine.rs | 38 +- .../tests/in_process_redirect/vlt.rs | 577 ++---------------- .../in_process_vendor_bun_takeover/vlt.rs | 39 +- .../tests/scan/hosted_symlinked_files.rs | 4 +- .../tests/vlt-leg-manifest.json | 2 - .../tests/vlt_e2e_common/fixture.rs | 9 +- .../tests/vlt_e2e_common/mod.rs | 18 - .../tests/vlt_hosted_common/mod.rs | 11 +- crates/socket-patch-core/src/api/client.rs | 6 - crates/socket-patch-core/src/hosted/engine.rs | 240 ++------ .../src/hosted/memory/stages.rs | 24 +- crates/socket-patch-core/src/hosted/mod.rs | 2 +- crates/socket-patch-core/src/hosted/vlt.rs | 183 +----- .../patch/redirect/group_equivalence_tests.rs | 1 - .../src/patch/redirect/mod.rs | 26 +- .../src/patch/redirect/vlt.rs | 26 +- .../src/patch/redirect/vlt_heal.rs | 178 +----- .../src/patch/redirect/vlt_preflight.rs | 527 ---------------- docs/ecosystems.md | 7 +- docs/testing/hosted-production-e2e.md | 17 +- docs/testing/vlt-compatibility.md | 21 +- docs/testing/vlt-coverage.json | 33 +- scripts/backtest-vlt.py | 107 +--- scripts/tests/test_backtest_harnesses.py | 114 +--- scripts/tests/test_ci_vlt_rows.py | 3 - scripts/tests/test_vlt_coverage.py | 8 +- 35 files changed, 267 insertions(+), 2379 deletions(-) delete mode 100644 crates/socket-patch-core/src/patch/redirect/vlt_preflight.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a6b071c44..41cfa3504 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2919,8 +2919,7 @@ jobs: --ignored --nocapture --test-threads=4 2>&1 | tee hosted-e2e.log || status=$? echo "::endgroup::" if [ "$status" -eq 0 ]; then - # The vlt leg (probe-driven: the clean refusal while the artifact - # is content-encoded, the full install proof once it is not). + # The vlt leg: the hosted pin and the fresh-checkout install proof. python3 scripts/check-vlt-legs.py \ --manifest crates/socket-patch-cli/tests/vlt-leg-manifest.json hosted-e2e.log exit $? diff --git a/.github/workflows/vlt-compatibility.yml b/.github/workflows/vlt-compatibility.yml index 3df73e0e1..26a51bb4e 100644 --- a/.github/workflows/vlt-compatibility.yml +++ b/.github/workflows/vlt-compatibility.yml @@ -466,10 +466,10 @@ jobs: # minutes (EAI_NONAME) while the service is up; see the action. uses: ./.github/actions/pin-socket-hosts - name: Backtest against production - # Every hosted cell probes the artifact first; it records - # blocked-by-server-encoding only when that probe saw a non-identity - # Content-Encoding and the CLI refused cleanly. Any other failure is an - # error. + # Every hosted cell probes the artifact first; a must-patch cell + # records blocked-by-server-encoding (without running the CLI) only + # when that probe saw a non-identity Content-Encoding. Any other + # failure is an error. shell: bash env: VLT_TEST_VERSION: ${{ matrix.vlt }} diff --git a/.github/workflows/vlt-serve-watchdog.yml b/.github/workflows/vlt-serve-watchdog.yml index 2bb63e4d7..484a6ea41 100644 --- a/.github/workflows/vlt-serve-watchdog.yml +++ b/.github/workflows/vlt-serve-watchdog.yml @@ -3,8 +3,8 @@ name: vlt serve watchdog # vlt hashes the wire body of a registry tarball and asks for # `accept-encoding: gzip;q=1.0, identity;q=0.5`, so a patch artifact that # patch.socket.dev (or its CDN) serves content-encoded fails `EINTEGRITY` on -# every vlt release, and hosted mode refuses to pin it -# (`redirect_vlt_artifact_unverifiable`). This job fetches the public free +# every vlt release (hosted mode pins it without downloading it, relying on +# the server's `Cache-Control: no-transform`). This job fetches the public free # minimist artifact exactly that way and fails when the response is # content-encoded or its sha512 differs from the API's integrity, which pages # through the repository's workflow-failure notification. diff --git a/crates/socket-patch-cli/CLI_CONTRACT.md b/crates/socket-patch-cli/CLI_CONTRACT.md index a3ed909d6..847779694 100644 --- a/crates/socket-patch-cli/CLI_CONTRACT.md +++ b/crates/socket-patch-cli/CLI_CONTRACT.md @@ -274,7 +274,7 @@ For a **9.0 root lock**, the CLI ensures `pnpm-workspace.yaml` carries `trustLoc `redirect_pnpm_no_lockfile` names pnpm when installer markers exist without a lock; `redirect_pnpm_entry_vendored` identifies a vendored entry instead of reporting it missing. Supported `shrinkwrap.yaml` files are writable lockfiles, not read-only markers. -**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`, or `redirect_vlt_custom_registry_skipped` alone). 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 version document of the registry the node resolves against (slot [3] is its `dist.tarball`, as vlt writes it; `upstream_registry_fallback` when that registry can't be read), following the lock's own slot-[3] convention (see "Hosted unwind coverage"). Tested releases: `docs/testing/vlt-compatibility.md`. +**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`, or `redirect_vlt_custom_registry_skipped` alone). 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. **Artifacts**: the scan never downloads the hosted artifacts; the patch server sends them unencoded (`Cache-Control: no-transform`), and vlt verifies slot [2] against the bytes it receives at install time. **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 version document of the registry the node resolves against (slot [3] is its `dist.tarball`, as vlt writes it; `upstream_registry_fallback` when that registry can't be read), 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)**: plain `vendor` with at least one hosted pin in scope is not a per-purl takeover: it EJECTS exactly the lockfile's hosted pins as one planned transition (see "Vendor command contract") and ignores `.socket/manifest.json` even when it exists — a manifest-only patch is not vendored, and a manifest naming a different patch for a hosted purl does not displace the hosted one (vendoring the manifest's patches takes a full `scan --mode vendored`, whose edits then land in the lockfiles). The eject runs the same upstream restore and the same backend previews and refusals described below, but reports them as an eject (`eject_planned` / `eject_refused` / `eject_rolled_back`) rather than through the per-purl takeover advisories. Vendoring over a hosted pin with `scan --mode vendored` / `get --mode vendored` (and the legacy manifest-driven `vendor` path, reached only when no hosted pin is in scope) 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 ` was hosted; restored its upstream registry entry () before vendoring (mode takeover)`; the human path prints `Warning: …`), plus any advisory the restore raised (`npm_allow_remote_left`, …). **A takeover the vendored backend does not carry through keeps the hosted pin (#853, #944)**: the wet run holds the restore in its group commit, and when the backend then refuses the purl — whatever the code: a pnpm `catalog:` dependency (`vendor_lock_entry_unsupported`), a CRLF `pnpm-lock.yaml` (`vendor_lockfile_crlf_unsupported`), a conflicting `pnpm-workspace.yaml` override — a range, another version or a `>` selector (`vendor_override_conflict`), a uv inline `[tool.uv] sources` table, a prebuilt download that fails, … — or its apply fails with nothing recorded, the restore is rolled back before anything reaches disk: the purl is reported `failed ` with the backend's own code and detail, neither `vendor_takeover_reverted_redirect` nor the restore's advisories are recorded for it, and the hosted wiring stays byte-for-byte (exit 1 / `partialFailure`), so the package stays hosted-patched instead of being un-hosted and then refused. `vendor --dry-run` previews that same `failed ` by running the backend's dry run over the restored project staged in memory (nothing written). A restore that writes a file outside the group commit's captured set (`.socket/gradle/hosted-index.tsv`) is not rolled back. The `scan` / `get --mode vendored --dry-run` preview, which stages no restore (it never touches the registry), predicts the pnpm (lockfileVersion 9) part of this (#853): a hosted pin the pnpm vendored backend refuses on lock or manifest text (a `catalog:` dependency, a CRLF `pnpm-lock.yaml` / `pnpm-workspace.yaml`, a conflicting override, …) is listed `would_refuse` with the wet run's `errorCode` and the backend's own `error` detail, evaluated on the still-hosted project (the pnpm restore only rewrites the entry's `resolution:`, which none of those gates reads); a hosted pin of any other lock flavor or ecosystem — a legacy pnpm 7/8 lock (`lockfileVersion` 5.4 / 6.0) included, whose CRLF, override-conflict and entry refusals are not lock-text gated — still previews `would_vendor` even when the wet takeover refuses it (the wet refusal keeps the hosted pin), except for the gem preflight below. **Gem preflight before the takeover**: `scan` / `get --mode vendored` ask the gem vendored backend's own refusals BEFORE the upstream restore — the manifest gate (`gemfile_not_loaded`: a `gems.rb` twin or a `BUNDLE_GEMFILE`-configured manifest) and the Gemfile declaration gate evaluated on the Gemfile and Gemfile.lock text the restore would leave (`gemfile_declaration_not_editable`: a declaration inside a `group` / `platforms` / conditional block, a parenthesized or duplicate declaration, …) — so a hosted gem vendored mode cannot wire is reported `failed ` with the hosted `Gemfile` / `Gemfile.lock` byte-untouched (exit 1 / `partialFailure`), and their `--dry-run` preview reports that gem as `would_refuse` with the same `errorCode` (exit 0, like the Bun / vlt `would_refuse` rows), never `would_vendor`. Pinned against real Bundler by `tests/e2e_redirect_gem_build.rs`. `--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 `, 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 to its upstream registry entry: ; restore it from version control instead (`git checkout -- `)` (for `bun.lock` / `bun.lockb` the detail also adds `, then run \`bun install --force\` (a plain \`bun install\` keeps the patched copy)`) (exit 1 / `partialFailure`, 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 ` with the hosted wiring and active Bun lock byte-untouched (exit 1 / `partialFailure`): 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 `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. @@ -455,7 +455,7 @@ patches: Supersession is judged on the by-package records the selection itself uses, so a scan with or without a cap never replaces an applied patch with an equal sibling (the tier and uuid tiebreaks and a missing date never count). When the lockfiles of a project pin a purl to several uuids, the recorded uuid is the selected one if it is among them, else the smallest. A hosted pin on a patch server that discovery does not recognize (an origin missing from `--patch-server-url`) still counts as ALREADY when a lockfile names the selected uuid, so the cap can never stall on it. -**Eligibility.** A NEW row is eligible only if every check the mode can decide without writing passes: the tier filter; the agent partition (vendor-owned and not-installed packages); the vendored Bun / vlt preflight; a `granted` hosted reference with a usable purl and url; the vlt artifact preflight; the vendored-to-hosted takeover refusals; wheel metadata; and a hosted rewrite whose confirmation probe shows a lockfile edit that pins it. Ineligible rows keep their own skip reasons and never hold a slot. References are fetched for every row before the budget is spent, and `--dry-run` fetches them too, so a dry run makes exactly the wet run's decisions. +**Eligibility.** A NEW row is eligible only if every check the mode can decide without writing passes: the tier filter; the agent partition (vendor-owned and not-installed packages); the vendored Bun / vlt preflight; a `granted` hosted reference with a usable purl and url; the vendored-to-hosted takeover refusals; wheel metadata; and a hosted rewrite whose confirmation probe shows a lockfile edit that pins it. Ineligible rows keep their own skip reasons and never hold a slot. References are fetched for every row before the budget is spent, and `--dry-run` fetches them too, so a dry run makes exactly the wet run's decisions. **Unit and order.** The budget counts distinct **base purls** (ecosystem + name + version, qualifiers stripped, percent-decoded; qualifier twins are one package): admitting one admits all of its eligible rows and costs one slot. Eligible NEW base purls are ranked by, ascending: in-flight first (in-memory `inFlightPatches` only); severity of the selected patch (critical, high, medium, low, unknown — the worst advisory it fixes); advisory count, descending; ecosystem name; base purl (bytewise); uuid. The order is total and has no time-dependent key (publish dates still pick the patch within a package, never the package order). @@ -1788,7 +1788,6 @@ command's integration tests check its output. | `redirect_vlt_no_lockfile` | `warnings[]` (warning) | scan/get `--mode hosted` (vlt): `vlt.json` or vlt's install state is present without `vlt-lock.json`; replaces `redirect_npm_no_lockfile` for vlt projects. | | `redirect_npm_shrinkwrap_only` | `warnings[]` (warning) | scan/get `--mode hosted` (npm, #899): the root `npm-shrinkwrap.json` is the only npm lock and carries a hosted redirect (this run's or an earlier one's — every run repeats it until the project gains a `package-lock.json`). npm >= 12 never reads the shrinkwrap and installs the unpatched registry bytes; npm <= 11 installs the patch. Rename the lock to `package-lock.json` (or commit a copy under that name) and re-run. | | `redirect_npm_deno_lock` | `warnings[]` (warning) | scan/get `--mode hosted` (npm, #406): one warning naming every redirected npm `name@version` (this run's or an earlier one's) that the root `deno.lock` also locks. `deno install` installs a `package.json` project's npm dependencies from `deno.lock` and never reads an npm-family lock, so installs Deno runs keep the registry copy and `vex` omits the patch (`vex_deno_lock_copy`); npm / pnpm / yarn / bun installs are patched, and `deno.lock` is never written. To patch what Deno installs, run `deno install` and then `socket-patch scan --mode agent`. | -| `redirect_vlt_artifact_unverifiable` | `warnings[]` (warning), `skipped` (hosted event) | scan/get `--mode hosted` (vlt): before any takeover or rewrite (dry runs included), each granted artifact with a default-registry instance in `vlt-lock.json` (or, for a purl a `flavor: "vlt"` vendored entry claims, its vendored node, probed before the takeover reverts it) is fetched once as vlt fetches it (`accept-encoding: gzip;q=1.0, identity;q=0.5`, no `Authorization`, up to 10 redirects) and must return 200 with no content encoding (or `identity`) and the granted sha512. On failure (`content-encoding `, `sha512 mismatch`, `http `, `fetch error `, `offline`) the dep is withheld from every rewriter when vlt drives or it is vlt-vendored (which also keeps it vendored), and from the vlt rewrite only otherwise (detail "…; vlt-lock.json was not changed for {purl}"; only the sibling lock this run rewrote can confirm it). A lock already pinned by an earlier run is left pinned, and neither confirmed nor attested. Projects without `vlt-lock.json` make no such request. The detail quotes the artifact URL (and any fetch error that echoes it) with its grant-token path level, the one just before the patch uuid, spelled ``; host, uuid and leaf stay. The in-memory hosted engine (`hosted-bundle`, the Node addon) has no network for this fetch, so it judges every in-scope artifact as `--offline` does (withheld, never pinned; the vendored takeover it refuses anyway). Exit 0. | | `redirect_vlt_reinstall_required` | `warnings[]` (advisory); rollback/remove `warnings[]` (+ human stderr) | vlt: `vlt-lock.json` pins (or, after rollback/remove, no longer pins) Socket-patched packages, and vlt never refreshes an installed copy. The heal removes `node_modules/.vlt-lock.json` and each stale `node_modules/.vlt/` of a Socket-owned node (never a link's target, never outside the project, never a copy it cannot judge) unless `--no-vlt-install-cleanup` or `--dry-run`. It never removes an optional node's copy (lock flags 1 or 3, or flags it cannot read): `vlt install` does not put a removed optional dependency back (its link dangles) unless the same install also reinstalls a non-optional node, so such a copy is left stale and the detail says to run `vlt ci` (or delete `node_modules` and run `vlt install`); vlt 0.0.0-30 … 1.0.4 install no optional dependency from the lock of a project that declares only optional dependencies, so there both commands remove the installed copy and the detail says to upgrade vlt to 1.0.5 or later first. The detail says whether copies were removed, left stale by a skipped cleanup, could not be checked, or none were stale, and adds how many optional copies were kept whenever there are any. The kept optional copies are named by what they are: `unpatched copies of optional dependencies` after `scan`/`get`, `patched copies of optional dependencies` after `rollback`/`remove`, and `installed copies of the vendored optional dependencies` after a hosted → vendored takeover (the copy the hosted pin left installed, which may still be the registry bytes). Stale or unchecked copies are not attested by the run's `--vex`, nor is a confirmed vlt pin the heal did not check (a URL on a host other than patch.socket.dev and the configured `--patch-server-url`/`--api-url`). A hidden lock that cannot be removed keeps every store entry. Invalidation failures only warn. | | `redirect_bun_reinstall_required` | rollback/remove `warnings[]` (+ human stderr), `vendor --revert` `warnings[]` | a wet hosted unwind that restored `bun.lock` / `bun.lockb` pins to the registry record while their installed copy may be kept (the `vendor_bun_reinstall_required` rule: Bun's hoisted linker keeps it through a plain `bun install`). One run-level warning naming every such `name@version` and `bun install --force` (or deleting `node_modules`). `vendor --revert` leaves it out when the vendored revert already advised `vendor_bun_reinstall_required` for every package it re-hosted. | | `vendor_pypi_reinstall_required` | `skipped` (advisory; human: `Warning: …`); rollback/remove `warnings[]`; `scan --prune` top-level `warnings[]` (human: `GC: …`) | a PyPI vendored revert (`vendor --revert`, rollback / remove of a vendored entry, the manifest reconcile, `--preserve-state` and `--dry-run` included) of a requirements.txt (pip), PDM, uv or Pipenv entry (#477, #1953). These tools reinstall a same-version package only when the LOCKED candidate is a URL or file that differs from the installed one, so once the lock is back on the registry release a plain `pdm sync` / `pdm install`, `uv sync` or `pipenv sync` reports nothing to do and keeps the vendored build; once a requirements line is back to `name==version`, `pip install -r` (and `uv pip install -r`) reports it already satisfied and keeps it too. The detail names the purl and the reinstall that restores the upstream bytes: `pip uninstall -y ` then `pip install -r ` for each requirements file that wired the entry (or the `uv pip uninstall` / `uv pip install -r` pair), which keeps those files' version ranges, index and hash options, `pdm sync --reinstall` (or recreating the virtualenv / `__pypackages__`), `uv sync --reinstall-package `, or Pipenv's `pipenv run pip uninstall -y && pipenv sync` (the categories from `Pipfile.lock`; `pipenv --rm && pipenv sync` for a clean virtualenv). Poetry, Hatch and PEP 751 entries do not get it. | diff --git a/crates/socket-patch-cli/src/commands/scan/hosted.rs b/crates/socket-patch-cli/src/commands/scan/hosted.rs index 37f1f4600..bbbefb94a 100644 --- a/crates/socket-patch-cli/src/commands/scan/hosted.rs +++ b/crates/socket-patch-cli/src/commands/scan/hosted.rs @@ -870,23 +870,6 @@ async fn run_redirect_selected_untracked( ); } - // vlt artifact preflight: before any takeover or rewrite (dry runs - // included), each in-scope artifact is fetched as vlt fetches it. A - // failure while vlt drives withholds the dep from every rewriter; - // otherwise only from the vlt rewrite. - let vlt_preflight = { - let deps: Vec<(&str, &DepOverride)> = candidates - .iter() - .filter(|c| c.dep.ecosystem == "npm") - .map(|c| (c.purl.as_str(), &c.dep)) - .collect(); - vlt::artifact_preflight(common, api_client, &deps).await - }; - engine::withhold_everywhere( - &mut candidates, - &vlt_preflight.withheld_everywhere, - &mut skipped, - ); // A NEW row the lockfiles already pin on the patch server these // references name (not a configured one, so the recorded view's // discovery could not see it) is ALREADY: decided here, before the lock @@ -1257,20 +1240,12 @@ async fn run_redirect_selected_untracked( .then(|| (read.clone(), python_metadata.clone())); // A retracted takeover rewrites again without it (see below). let takeover_metadata = takeover.is_staged().then(|| python_metadata.clone()); - let mut done = engine::rewrite( - &view, - read, - &candidates, - python_metadata, - &vlt_preflight.withheld_from_vlt, - rewrite_options(), - ) - .await; + let mut done = + engine::rewrite(&view, read, &candidates, python_metadata, rewrite_options()).await; // The rollout gate (§5.2): every write-free check has run — grants, - // purl/url, vlt preflight, takeover refusals, wheel metadata, and the - // rewrite above, whose confirmation probe proves a NEW row would be - // pinned. Only then is the budget spent; deferred rows leave the + // purl/url, takeover refusals, wheel metadata, and the rewrite above, + // whose confirmation probe proves a NEW row would be pinned. Only then is the budget spent; deferred rows leave the // rewrite set, which is rewritten again without them. if let (Some(gate), Some((read, python_metadata))) = (rollout.as_mut(), second_pass) { let eligible: std::collections::HashSet<(String, String)> = done @@ -1290,15 +1265,8 @@ async fn run_redirect_selected_untracked( let before = candidates.len(); candidates.retain(|c| !deferred.contains(&(sel_purl(c), c.dep.patch_uuid.clone()))); if candidates.len() != before { - done = engine::rewrite( - &view, - read, - &candidates, - python_metadata, - &vlt_preflight.withheld_from_vlt, - rewrite_options(), - ) - .await; + done = + engine::rewrite(&view, read, &candidates, python_metadata, rewrite_options()).await; } } // A staged takeover the rewrite did not pin must not happen: undo every @@ -1330,7 +1298,6 @@ async fn run_redirect_selected_untracked( read, &candidates, takeover_metadata.clone().unwrap_or_default(), - &vlt_preflight.withheld_from_vlt, rewrite_options(), ) .await; @@ -1553,7 +1520,6 @@ async fn run_redirect_selected_untracked( .get(lock_key) .or_else(|| done.files.get(lock_key)) .map(String::as_str), - preflight: &vlt_preflight, records: &records, confirmed: &confirmed, confirmed_vlt: &rewrite.confirmed_vlt_uuids, @@ -1729,7 +1695,6 @@ async fn run_redirect_selected_untracked( // rewriter's own warnings first (e.g. `no package-lock.json`), then the // record, package-manager, stale-install, takeover and prune warnings. let mut engine_warnings = rewrite.warnings.clone(); - engine_warnings.extend(vlt_preflight.warnings.iter().cloned()); engine_warnings.extend(record_warnings); engine_warnings.extend(done.rush_warnings.iter().cloned()); engine_warnings.extend(done.pnpm_warnings.iter().cloned()); @@ -2234,9 +2199,6 @@ fn describe_skip_reason(reason: &str) -> String { (see the warning)" .into() } - "redirect_vlt_artifact_unverifiable" => { - "vlt could not verify the hosted artifact (see the warning)".into() - } "redirect_unattributable" => { "lockfile discovery could not attribute its pin to one package version, so \ nothing was written for it (reconcile the project's lockfiles; --json has the \ @@ -3808,10 +3770,6 @@ mod tests { describe_skip_reason("redirect_bun_lockb_invalid"), describe_skip_reason("redirect_bun_lock_unsupported") ); - assert_eq!( - describe_skip_reason("redirect_vlt_artifact_unverifiable"), - "vlt could not verify the hosted artifact (see the warning)" - ); assert_eq!( describe_skip_reason("redirect_vlt_lock_unsupported"), "vlt-lock.json blocks the vendored-to-hosted migration (see the warning)" diff --git a/crates/socket-patch-cli/src/commands/vlt_heal.rs b/crates/socket-patch-cli/src/commands/vlt_heal.rs index 64e4992de..48504d129 100644 --- a/crates/socket-patch-cli/src/commands/vlt_heal.rs +++ b/crates/socket-patch-cli/src/commands/vlt_heal.rs @@ -1,23 +1,18 @@ -//! The vlt steps shared by the hosted flow and the unwinds: the artifact -//! preflight, run before any takeover or rewrite, and the warm-tree heal -//! with its `redirect_vlt_reinstall_required` advisory, run after a hosted +//! The vlt warm-tree heal shared by the hosted flow and the unwinds, with +//! its `redirect_vlt_reinstall_required` advisory: run after a hosted //! rewrite, after rollback/remove restore the vlt pins, and after a //! hosted→vendored takeover. A helper module, not a command: `scan`, //! `rollback`, `remove` and `vendor` all reach it without importing each //! other. use std::collections::{BTreeMap, BTreeSet}; -use std::path::Path; use socket_patch_core::constants::npm_family::{VLT_HIDDEN_LOCK_REL, VLT_LOCK}; -use socket_patch_core::hosted::vlt::{self as hosted_vlt, Preflight}; use socket_patch_core::manifest::schema::PatchRecord; +use socket_patch_core::patch::redirect::vlt; use socket_patch_core::patch::redirect::vlt_heal::{ self, classify_target, read_install_state, Expected, LedgerTarget, Target, TargetState, }; -use socket_patch_core::patch::redirect::vlt_preflight; -use socket_patch_core::patch::redirect::{vlt, DepOverride}; -use socket_patch_core::vendor::lock_inventory::ProjectView; /// What the post-rewrite heal reports back to the hosted flow: its /// advisories, and the confirmed purls whose installed or next-installed @@ -36,52 +31,6 @@ pub(crate) const REINSTALL_REQUIRED: &str = "redirect_vlt_reinstall_required"; /// A bundled copy of a redirected package that no rewire reaches. pub(crate) const BUNDLED_INSTANCE_SKIPPED: &str = "redirect_vlt_bundled_instance_skipped"; -/// The uuids of `deps` whose purl a vlt vendored ledger entry claims: a -/// hosted takeover reverts them to a registry node before the rewrite. -async fn vlt_vendored_uuids(cwd: &Path, deps: &[(&str, &DepOverride)]) -> BTreeSet { - let Ok(state) = socket_patch_core::vendor::load_state(cwd).await else { - return BTreeSet::new(); - }; - deps.iter() - .filter(|(purl, _)| { - socket_patch_core::vendor::lookup_entry( - &state.entries, - socket_patch_core::utils::purl::strip_purl_qualifiers(purl), - ) - .is_some_and(|e| e.ecosystem == "npm" && e.flavor.as_deref() == Some("vlt")) - }) - .map(|(_, dep)| dep.patch_uuid.clone()) - .collect() -} - -/// Fetch each in-scope artifact the way vlt does (once per distinct URL, -/// `offline` making no request) and decide which deps may be pinned in -/// `vlt-lock.json` ([`socket_patch_core::hosted::vlt`]). Projects without -/// `vlt-lock.json` make no request. A vlt-vendored dep is probed through -/// its vendored node, before the takeover reverts it, and a failure keeps -/// it vendored. -pub(crate) async fn artifact_preflight( - common: &crate::args::GlobalArgs, - api_client: &socket_patch_core::api::client::ApiClient, - deps: &[(&str, &DepOverride)], -) -> Preflight { - let view = ProjectView::Disk(&common.cwd); - let files = hosted_vlt::inputs(&view).await; - if files.is_empty() { - return Preflight::default(); - } - let vendored = vlt_vendored_uuids(&common.cwd, deps).await; - let Some(plan) = hosted_vlt::plan(&view, &files, deps, &vendored) else { - return Preflight::default(); - }; - let probes = if common.offline { - BTreeMap::new() - } else { - vlt_preflight::probe_artifacts(api_client, &plan.urls()).await - }; - hosted_vlt::judge(&plan, deps, probes) -} - /// The origins the vlt heal treats as Socket-owned in the final lock: the /// patch server plus `--api-url`. Unlike discovery's /// [`crate::commands::hosted_unwind::patch_server_origins`], it also counts the @@ -307,8 +256,8 @@ fn takeover_detail(tally: &HealTally, vendored: usize) -> Option { fn undeterminable_detail(n: usize) -> String { format!( "vlt-lock.json pins Socket-patched packages, but socket-patch could not check {n} \ - installed copies (node_modules is a link, or no patch record or artifact was \ - available); run `vlt ci` to be sure the patched packages are installed." + installed copies (node_modules is a link, or no patch record was available); run \ + `vlt ci` to be sure the patched packages are installed." ) } @@ -316,7 +265,6 @@ fn undeterminable_detail(n: usize) -> String { pub(crate) struct HealInputs<'a> { /// The final `vlt-lock.json` (as written, or as a dry run would write it). pub(crate) final_lock: Option<&'a str>, - pub(crate) preflight: &'a Preflight, /// This run's fetched records merged with the ledger's, keyed by purl. pub(crate) records: &'a BTreeMap, pub(crate) confirmed: &'a [(String, String)], @@ -329,8 +277,7 @@ pub(crate) struct HealInputs<'a> { /// installed or next-installed bytes are not known to be patched (removed /// from the same run's in-run VEX attestation). A confirmed vlt uuid with /// no heal target (a non-Socket host, a leaf that disagrees with the -/// DepID, an artifact no preflight verified) was never checked, so it is -/// never attested here. +/// DepID) was never checked, so it is never attested here. pub(crate) async fn heal_after_rewrite( common: &crate::args::GlobalArgs, inputs: &HealInputs<'_>, @@ -341,7 +288,7 @@ pub(crate) async fn heal_after_rewrite( .map(|lock| vlt_heal::socket_owned_instances(lock, &vlt_heal_origins(common))) .unwrap_or_default() .into_iter() - .filter(|i| inputs.preflight.passed.contains(&i.patch_uuid)) + .filter(|i| inputs.confirmed_vlt.contains(&i.patch_uuid)) .collect(); let targeted: BTreeSet<&str> = owned.iter().map(|i| i.patch_uuid.as_str()).collect(); let mut tally = HealTally::default(); @@ -357,7 +304,6 @@ pub(crate) async fn heal_after_rewrite( name: &i.name, lock_sha512: i.sha512.as_deref(), record, - artifact: inputs.preflight.artifacts.get(&i.url).map(Vec::as_slice), flags: i.flags, }, i.patch_uuid.as_str(), @@ -458,7 +404,6 @@ pub(crate) async fn rollback_heal( name: &t.name, lock_sha512: sha.as_deref(), record: t.record.as_ref(), - artifact: None, flags: *flags, }, t.purl.as_str(), @@ -492,7 +437,6 @@ pub(crate) async fn takeover_heal( name: &t.name, lock_sha512: None, record: t.record.as_ref(), - artifact: None, flags: t.flags, }, t.purl.as_str(), @@ -508,16 +452,6 @@ pub(crate) async fn takeover_heal( mod tests { use super::*; - fn test_api() -> socket_patch_core::api::client::ApiClient { - socket_patch_core::api::client::ApiClient::new( - socket_patch_core::api::client::ApiClientOptions { - api_url: "http://127.0.0.1:9".into(), - api_token: Some("secret".into()), - route: socket_patch_core::api::client::ApiRoute::org("org"), - }, - ) - } - #[test] fn advisory_variants_follow_severity_order() { let mut tally = HealTally::default(); @@ -621,134 +555,4 @@ mod tests { ); assert_eq!(takeover_detail(&HealTally::default(), 1), None); } - - fn left_pad_dep(url: &str) -> DepOverride { - DepOverride { - ecosystem: "npm".into(), - name: "left-pad".into(), - namespace: None, - version: "1.3.0".into(), - token: String::new(), - patch_uuid: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa".into(), - artifact_url: url.to_string(), - registry_override: None, - integrity: socket_patch_core::patch::redirect::Integrity { - sha512: Some("sha512-new".into()), - ..Default::default() - }, - } - } - - const LEFT_PAD_LOCK: &str = "{\n \"lockfileVersion\": 1,\n \"options\": {},\n \"nodes\": \ - {\n \"~npm~left-pad@1.3.0\": [0,\"left-pad\",\"sha512-old\",\ - \"https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz\"]\n },\n \"edges\": {}\n}\n"; - - #[tokio::test] - async fn offline_withholds_every_probed_dep_without_a_request() { - let server = wiremock::MockServer::start().await; - let tmp = tempfile::tempdir().unwrap(); - let url = format!("{}/patch/npm/t/u/left-pad-1.3.0.tgz", server.uri()); - std::fs::write(tmp.path().join(VLT_LOCK), LEFT_PAD_LOCK).unwrap(); - let dep = left_pad_dep(&url); - let common = crate::args::GlobalArgs { - cwd: tmp.path().to_path_buf(), - offline: true, - ..crate::args::GlobalArgs::default() - }; - let api = test_api(); - let pre = artifact_preflight(&common, &api, &[("pkg:npm/left-pad@1.3.0", &dep)]).await; - assert!(server.received_requests().await.unwrap().is_empty()); - assert!(pre.passed.is_empty()); - assert_eq!( - pre.withheld_everywhere - .get(&dep.patch_uuid) - .map(String::as_str), - Some("pkg:npm/left-pad@1.3.0") - ); - assert_eq!( - pre.warnings[0].detail, - format!( - "vlt would fail to verify {url}: offline; nothing was written for \ - pkg:npm/left-pad@1.3.0" - ) - ); - } - - #[tokio::test] - async fn no_vlt_lock_makes_no_request() { - let server = wiremock::MockServer::start().await; - let url = format!("{}/patch/npm/t/u/left-pad-1.3.0.tgz", server.uri()); - let dep = left_pad_dep(&url); - let api = test_api(); - for with_lock in [false, true] { - let tmp = tempfile::tempdir().unwrap(); - std::fs::write(tmp.path().join("package-lock.json"), "{}").unwrap(); - if with_lock { - std::fs::write(tmp.path().join(VLT_LOCK), LEFT_PAD_LOCK).unwrap(); - } - let common = crate::args::GlobalArgs { - cwd: tmp.path().to_path_buf(), - ..crate::args::GlobalArgs::default() - }; - let pre = artifact_preflight(&common, &api, &[("pkg:npm/left-pad@1.3.0", &dep)]).await; - let requests = server.received_requests().await.unwrap().len(); - if with_lock { - assert_eq!(requests, 1, "the control probes the vlt lock's dep"); - assert!(pre.withheld_from_vlt.contains(&dep.patch_uuid)); - } else { - assert_eq!(requests, 0); - assert!(pre.passed.is_empty() && pre.warnings.is_empty()); - assert!(pre.withheld_everywhere.is_empty() && pre.withheld_from_vlt.is_empty()); - } - } - } - - #[tokio::test] - async fn a_vlt_vendored_dep_is_probed_and_withheld_everywhere() { - let server = wiremock::MockServer::start().await; - let url = format!("{}/patch/npm/t/u/left-pad-1.3.0.tgz", server.uri()); - let dep = left_pad_dep(&url); - let tmp = tempfile::tempdir().unwrap(); - std::fs::write(tmp.path().join("package-lock.json"), "{}").unwrap(); - std::fs::write( - tmp.path().join(VLT_LOCK), - "{\n \"lockfileVersion\": 1,\n \"options\": {},\n \"nodes\": {\n \ - \"file~.socket+vendor+npm+11111111-2222-4333-8444-555555555555+left-pad-1.3.0+node__modules+left-pad\": \ - [0,\"left-pad\",null,\".socket/vendor/npm/11111111-2222-4333-8444-555555555555/left-pad-1.3.0/node_modules/left-pad\"]\n \ - },\n \"edges\": {}\n}\n", - ) - .unwrap(); - let common = crate::args::GlobalArgs { - cwd: tmp.path().to_path_buf(), - ..crate::args::GlobalArgs::default() - }; - let api = test_api(); - let deps = [("pkg:npm/left-pad@1.3.0", &dep)]; - let unclaimed = artifact_preflight(&common, &api, &deps).await; - assert!(server.received_requests().await.unwrap().is_empty()); - assert!(unclaimed.warnings.is_empty()); - std::fs::create_dir_all(tmp.path().join(".socket/vendor")).unwrap(); - std::fs::write( - tmp.path().join(".socket/vendor/state.json"), - serde_json::to_vec(&serde_json::json!({ - "version": 1, - "entries": { "pkg:npm/left-pad@1.3.0": { - "ecosystem": "npm", "basePurl": "pkg:npm/left-pad@1.3.0", - "uuid": "11111111-2222-4333-8444-555555555555", - "artifact": { "path": ".socket/vendor/npm/11111111-2222-4333-8444-555555555555/left-pad-1.3.0" }, - "wiring": [], "flavor": "vlt" - }} - })) - .unwrap(), - ) - .unwrap(); - let claimed = artifact_preflight(&common, &api, &deps).await; - assert_eq!(server.received_requests().await.unwrap().len(), 1); - assert!(claimed.withheld_from_vlt.is_empty()); - assert!(claimed.withheld_everywhere.contains_key(&dep.patch_uuid)); - assert_eq!( - claimed.warnings[0].detail, - format!("vlt would fail to verify {url}: http 404; nothing was written for pkg:npm/left-pad@1.3.0") - ); - } } diff --git a/crates/socket-patch-cli/tests/e2e_hosted_production.rs b/crates/socket-patch-cli/tests/e2e_hosted_production.rs index a2896d0eb..44e946b7f 100644 --- a/crates/socket-patch-cli/tests/e2e_hosted_production.rs +++ b/crates/socket-patch-cli/tests/e2e_hosted_production.rs @@ -2621,20 +2621,10 @@ async fn canary_unpublished_ecosystems() { // vlt (suite `production`, DESIGN §8.3) // --------------------------------------------------------------------------- -/// `SOCKET_PATCH_VLT_HOSTED_PRODUCTION_REQUIRED=1`: the serve fix is -/// deployed, so a content-encoded artifact is a failure, not a pinned -/// refusal. -fn vlt_hosted_production_required() -> bool { - std::env::var("SOCKET_PATCH_VLT_HOSTED_PRODUCTION_REQUIRED").is_ok_and(|v| v == "1") -} - -/// The vlt install proof against production. The artifact is fetched the -/// way vlt fetches it (`fetch_artifact_probe`): while patch.socket.dev -/// re-encodes it, the scan must refuse cleanly -/// (`redirect_vlt_artifact_unverifiable`, nothing written); once it serves -/// identity bytes, the lock pins it and a fresh checkout's `vlt ci` -/// installs the patched minimist. So the test neither breaks nor goes -/// vacuous when the serve fix deploys. +/// The vlt install proof against production: the scan pins the hosted +/// artifact in `vlt-lock.json`, and a fresh checkout's `vlt ci` (which +/// verifies the lock's sha512 against the bytes it receives) installs the +/// patched minimist. #[tokio::test(flavor = "multi_thread")] #[ignore = "live production API + real npm registry + real vlt. Run with --ignored."] async fn vlt_pinned_matrix_production_hosted_install_proof() { @@ -2658,7 +2648,6 @@ async fn vlt_pinned_matrix_production_hosted_install_proof() { PATCH_MARKER, "vlt hosted production", ); - let lock_before = lock_bytes(&proj); std::fs::remove_dir_all(proj.join("node_modules")).unwrap(); let cwd = proj.to_str().unwrap().to_string(); let out = socket( @@ -2666,69 +2655,16 @@ async fn vlt_pinned_matrix_production_hosted_install_proof() { &["scan", "--mode", "hosted", "--json", "--yes", "--cwd", &cwd], &[], ); - let doc = out.json(); + assert_eq!(out.code, 0, "{out}"); let lock = read_lock(&proj); let id = node_id(&lock, NPM_NAME, NPM_VERSION); - let pinned_url = lock["nodes"][&id][3].as_str().map(str::to_string); - // A refusal quotes the artifact URL with its grant token redacted, so - // the URL to probe comes from the public reference endpoint scan used. - let url = match pinned_url.filter(|u| u.contains(PATCH_HOST)) { - Some(u) => u, - None => public_reference_url(NPM_UUID).await, - }; - assert!(url.contains(NPM_UUID), "{url}"); - let client = reqwest::Client::builder().build().unwrap(); - let probe = - socket_patch_core::patch::redirect::vlt_preflight::fetch_artifact_probe(&client, &url) - .await; - let encoded = probe - .content_encoding - .as_deref() - .is_some_and(|e| !e.trim().is_empty() && !e.trim().eq_ignore_ascii_case("identity")); - if encoded { - assert!( - !vlt_hosted_production_required(), - "patch.socket.dev still serves {url} content-encoded ({:?}) but \ - SOCKET_PATCH_VLT_HOSTED_PRODUCTION_REQUIRED=1", - probe.content_encoding - ); - let detail = warning_detail(&doc, "redirect_vlt_artifact_unverifiable"); - assert!( - detail.contains("content-encoding") && detail.contains("nothing was written"), - "{detail}" - ); - assert_eq!(lock_bytes(&proj), lock_before, "the refusal writes nothing"); - assert!(!proj.join(".socket/vendor/redirect-state.json").exists()); - eprintln!("vlt hosted production: artifact content-encoded; clean refusal pinned"); - } else { - assert_eq!(out.code, 0, "{out}"); - assert_eq!( - lock["nodes"][&id][2].as_str(), - probe.sha512.as_deref(), - "{lock:#}" - ); - let co = fresh_checkout(&proj, &leg.root.join("fresh")); - leg.vlt_ok_with(&co, &leg.locked_install_args(), &VltRun::profile("fresh")); - assert_patched(&minimist_entry(&co), PATCH_MARKER, "vlt hosted production"); - } + let pinned_url = lock["nodes"][&id][3].as_str().unwrap_or_default(); + assert!( + pinned_url.contains(PATCH_HOST) && pinned_url.contains(NPM_UUID), + "{lock:#}" + ); + let co = fresh_checkout(&proj, &leg.root.join("fresh")); + leg.vlt_ok_with(&co, &leg.locked_install_args(), &VltRun::profile("fresh")); + assert_patched(&minimist_entry(&co), PATCH_MARKER, "vlt hosted production"); leg.ran(); } - -/// The hosted artifact URL the public proxy grants for `uuid` — the -/// reference `scan --mode hosted` resolves on a token-less run. -async fn public_reference_url(uuid: &str) -> String { - use socket_patch_core::api::client::{ApiClient, ApiClientOptions}; - let client = ApiClient::new(ApiClientOptions { - api_url: PROXY.to_string(), - api_token: None, - route: socket_patch_core::api::client::ApiRoute::Proxy, - }); - let references = client - .fetch_registry_references(&[uuid.to_string()]) - .await - .expect("the public proxy answers the reference request"); - references - .get(uuid) - .and_then(|r| r.url.clone()) - .expect("the public proxy grants the free patch's artifact URL") -} diff --git a/crates/socket-patch-cli/tests/e2e_redirect_vlt_build.rs b/crates/socket-patch-cli/tests/e2e_redirect_vlt_build.rs index 1b89b499f..cbea59cc2 100644 --- a/crates/socket-patch-cli/tests/e2e_redirect_vlt_build.rs +++ b/crates/socket-patch-cli/tests/e2e_redirect_vlt_build.rs @@ -52,8 +52,8 @@ fn assert_restored_lock(fx: &Fixture, before: &[u8]) { // ── drivers and fresh checkouts ─────────────────────────────────────────── -/// `scan --mode hosted --vex`: the lock pins the artifact (one preflight -/// GET), in-run VEX attests it unless a lock-level warning withholds it, +/// `scan --mode hosted --vex`: the lock pins the artifact (never fetched by +/// the scan), in-run VEX attests it unless a lock-level warning withholds it, /// and a fresh checkout's locked install lands the patched bytes with the /// lock byte-stable. #[tokio::test(flavor = "multi_thread")] @@ -69,7 +69,11 @@ async fn vlt_pinned_matrix_hosted_scan_fresh_ci() { fx.assert_lock_warnings(&doc); fx.assert_in_run_vex(&out, fx.t(), fx.in_run_vex_attests()); assert_pinned(&fx.proj, &fx.svc, fx.t()); - assert_eq!(fx.svc.artifact_hits(fx.t()).await, 1, "one preflight GET"); + assert_eq!( + fx.svc.artifact_hits(fx.t()).await, + 0, + "the scan fetches no artifact" + ); fx.assert_fresh_locked_install("fresh-ci"); fx.leg.ran(); } @@ -365,8 +369,9 @@ async fn vlt_pinned_matrix_hosted_heal_rule_c_no_hidden_lock() { fx.leg.ran(); } -/// Heal rule (c) with no patch record (the view route fails): the -/// preflight-downloaded artifact bytes decide. +/// Heal rule (c) with no patch record (the view route fails) and no hidden +/// lock: the installed copies cannot be judged, so they are kept and the +/// advisory says to run `vlt ci`. #[tokio::test(flavor = "multi_thread")] #[ignore = "real vlt: SOCKET_PATCH_VLT_E2E_JS"] async fn vlt_pinned_matrix_hosted_heal_rule_c_no_record() { @@ -384,15 +389,11 @@ async fn vlt_pinned_matrix_hosted_heal_rule_c_no_record() { .mount(&fx.svc.server) .await; let ids = fx.store_ids(fx.t()); - let snap = fx.snapshot(&fx.proj, &ids); let doc = fx.scan(&[]); - fx.assert_advisory(&doc, &advisory_invalidated(ids.len())); + fx.assert_advisory(&doc, &advisory_undeterminable(ids.len())); for id in &ids { - assert!(!store_entry(&fx.proj, id).exists(), "{id} invalidated"); + assert!(store_entry(&fx.proj, id).exists(), "{id} kept"); } - snap.assert_same(&fx.snapshot(&fx.proj, &ids), "the artifact-bytes heal"); - fx.vlt_ok(&fx.proj, &["install"]); - assert_eq!(state(&fx.proj, fx.t()), State::Patched); fx.leg.ran(); } @@ -1097,28 +1098,6 @@ async fn vlt_pinned_matrix_hosted_registry_from_user_config() { fx.leg.ran(); } -/// The artifact comes back `Content-Encoding: gzip` (patch.socket.dev -/// before the `no-transform` fix): refused, nothing written. -#[tokio::test(flavor = "multi_thread")] -#[ignore = "real vlt: SOCKET_PATCH_VLT_E2E_JS"] -async fn vlt_pinned_matrix_hosted_content_encoding_refused() { - let Some(leg) = hosted_leg("content_encoding_refused") else { - return; - }; - let fx = Fixture::build(leg, Shape::left_pad()).await; - fx.svc.serve_artifact_gzip_encoded(fx.t()).await; - let out = socket_api(&fx.proj, &fx.svc, &["scan", "--mode", "hosted"], &[]); - let doc = out.json(); - let detail = warning_detail(&doc, UNVERIFIABLE); - assert!( - detail.contains("content-encoding gzip") && detail.contains("nothing was written"), - "{detail}" - ); - assert_eq!(lock_bytes(&fx.proj), fx.lock_before, "no lock write"); - assert!(fx.ledger().is_none(), "no ledger"); - fx.leg.ran(); -} - /// 0.0.0-16 … 24 without `"modifiers": {}` ignore the lock: the warning /// fires and the install is silently unpatched (pinned). #[tokio::test(flavor = "multi_thread")] diff --git a/crates/socket-patch-cli/tests/e2e_vex_lockfile/vlt.rs b/crates/socket-patch-cli/tests/e2e_vex_lockfile/vlt.rs index 7b76accbd..a2ce34525 100644 --- a/crates/socket-patch-cli/tests/e2e_vex_lockfile/vlt.rs +++ b/crates/socket-patch-cli/tests/e2e_vex_lockfile/vlt.rs @@ -8,9 +8,9 @@ //! (`lockfileVersion: 1` tilde ids, `0` legacy `·npm·` ids, and the //! version-less `··` ids): //! -//! * hosted — the REAL `scan --mode hosted` (a wiremock patch API that also -//! serves the artifact the vlt preflight fetches, so the hosted URL is on -//! the mock origin and `vex` gets it as `--patch-server-url`) rewrites the +//! * hosted — the REAL `scan --mode hosted` (a wiremock patch API whose +//! granted artifact URL is on the mock origin, so `vex` gets it as +//! `--patch-server-url`) rewrites the //! lock over an importer copy; then vlt's installed state is staged (the //! importer copy and `node_modules/.vlt//node_modules/left-pad`) //! and, with the manifest deleted: diff --git a/crates/socket-patch-cli/tests/hosted_memory_engine.rs b/crates/socket-patch-cli/tests/hosted_memory_engine.rs index c9f771b64..032272599 100644 --- a/crates/socket-patch-cli/tests/hosted_memory_engine.rs +++ b/crates/socket-patch-cli/tests/hosted_memory_engine.rs @@ -1043,13 +1043,10 @@ async fn result_serializes_with_the_contract_keys() { } } -/// A vlt project: the engine has no network for the artifact preflight the -/// disk flow runs, so every in-scope dep is judged as `--offline` judges -/// it. vlt drives here, so the dep is withheld from every rewriter -/// (`redirect_vlt_artifact_unverifiable`) and nothing is written. The -/// warning quotes the artifact URL with its grant-token level redacted. +/// A vlt project: the engine pins the hosted artifact in `vlt-lock.json` +/// exactly as the disk flow does (no artifact is fetched by either). #[tokio::test] -async fn a_vlt_project_is_withheld_as_offline() { +async fn a_vlt_project_is_pinned() { const VLT_FIXTURE: &str = "redirect/npm/vlt/basic"; let server = MockServer::start().await; let patches = patches_from_overrides( @@ -1057,29 +1054,22 @@ async fn a_vlt_project_is_withheld_as_offline() { None, ); mount_api(&server, &patches).await; - let files = fixture_files(&fixtures_root().join(VLT_FIXTURE).join("input")); + let fixture = fixtures_root().join(VLT_FIXTURE); + let files = fixture_files(&fixture.join("input")); let output = run_engine(&server, build_input(&files, &[], options(false))).await; let project = &output.projects[0]; - assert!(project.error.is_none(), "the vlt root is scanned"); - assert!(project.redirected.is_empty(), "nothing is pinned"); - assert!(project - .skipped - .iter() - .any(|s| s.reason == "redirect_vlt_artifact_unverifiable")); - let warnings = project.redirect["warnings"].as_array().unwrap(); - let detail = warnings + assert!(project.error.is_none(), "{:?}", project.error); + assert_eq!(project.redirected.len(), 1, "{:?}", project.skipped); + assert!(project.skipped.is_empty(), "{:?}", project.skipped); + let expected = std::fs::read_to_string(fixture.join("expected/vlt-lock.json")).unwrap(); + let changed: Vec<(&str, &str)> = output + .changed_files .iter() - .find(|w| w["code"] == "redirect_vlt_artifact_unverifiable") - .and_then(|w| w["detail"].as_str()) - .expect("the preflight warning is reported"); - assert!( - detail.contains("/patch/npm//") - && detail.contains(": offline; nothing was written"), - "the offline refusal quotes the redacted URL" - ); - assert!(output.changed_files.is_empty()); + .map(|f| (f.path.as_str(), f.content.as_str())) + .collect(); + assert_eq!(changed, vec![("vlt-lock.json", expected.as_str())]); } const GEM_BASIC_FIXTURE: &str = "redirect/gem/bundler/basic"; diff --git a/crates/socket-patch-cli/tests/in_process_redirect/vlt.rs b/crates/socket-patch-cli/tests/in_process_redirect/vlt.rs index 60a00a16f..6d0510734 100644 --- a/crates/socket-patch-cli/tests/in_process_redirect/vlt.rs +++ b/crates/socket-patch-cli/tests/in_process_redirect/vlt.rs @@ -1,6 +1,5 @@ //! Hosted vlt legs (DESIGN §3, §8.2): the rewrite through the CLI, the -//! artifact preflight, the warm-tree heal with its advisory, and the in-run -//! VEX exclusion. Every run is the scrubbed subprocess binary, so the +//! warm-tree heal with its advisory, and the in-run VEX exclusion. Every run is the scrubbed subprocess binary, so the //! `--json` envelope is read back and no parent env is mutated. use std::path::Path; @@ -49,7 +48,7 @@ async fn assert_rewrites(era: Era, expected_warnings: &[&str]) { assert_eq!(warning_detail(&doc, ADVISORY), ADVISORY_NOTHING_STALE); // v5: the lock pin is the whole record — no redirect ledger. assert_no_ledger(tmp.path()); - assert_eq!(artifact_requests(&server).await, 1); + assert_eq!(artifact_requests(&server).await, 0); } #[tokio::test] @@ -160,11 +159,7 @@ async fn scan_redirect_vlt_peer_instances() { ) ); assert_no_ledger(tmp.path()); - assert_eq!( - artifact_requests(&server).await, - 1, - "one GET per distinct artifact URL" - ); + assert_eq!(artifact_requests(&server).await, 0); } #[tokio::test] @@ -214,202 +209,7 @@ async fn scan_redirect_vlt_sibling_package_lock_vlt_installed_does_not_confirm_r assert_no_ledger(tmp.path()); } -// ── artifact preflight ─────────────────────────────────────────────────── - -async fn assert_preflight_refuses(response: ResponseTemplate, reason: &str) { - let server = MockServer::start().await; - mock_discovery(&server).await; - mock_reference(&server).await; - mock_view(&server).await; - mock_artifact_with(&server, response).await; - let tmp = tempfile::tempdir().unwrap(); - write_vlt_project(tmp.path(), Era::V1); - let lock = read(tmp.path(), "vlt-lock.json"); - - let (_, doc) = scan_hosted(tmp.path(), &server, &[], &[]); - - assert_eq!(redirected(&doc), 0, "{doc:#}"); - assert_eq!(read(tmp.path(), "vlt-lock.json"), lock); - assert!(!ledger_path(tmp.path()).exists(), "nothing is written"); - assert_eq!( - warning_detail(&doc, UNVERIFIABLE), - format!( - "vlt would fail to verify {}: {reason}; nothing was written for {PURL}", - redacted_artifact_url(&server) - ) - ); - assert_eq!(skipped_reasons(&doc), [UNVERIFIABLE]); - assert!(!warning_codes(&doc).contains(&ADVISORY.to_string())); -} - -#[tokio::test] -async fn scan_redirect_vlt_artifact_content_encoding_refused() { - assert_preflight_refuses( - ResponseTemplate::new(200) - .insert_header("content-encoding", "gzip") - .set_body_bytes(gzip(&patched_tarball())), - "content-encoding gzip", - ) - .await; -} - -#[tokio::test] -async fn scan_redirect_vlt_artifact_identity_passes() { - let server = MockServer::start().await; - mock_discovery(&server).await; - mock_reference(&server).await; - mock_view(&server).await; - mock_artifact_with( - &server, - ResponseTemplate::new(200) - .insert_header("content-encoding", "identity") - .set_body_bytes(patched_tarball()), - ) - .await; - let tmp = tempfile::tempdir().unwrap(); - write_vlt_project(tmp.path(), Era::V1); - - let (_, doc) = scan_hosted(tmp.path(), &server, &[], &[]); - - assert_eq!(redirected(&doc), 1); - assert!(!warning_codes(&doc).contains(&UNVERIFIABLE.to_string())); -} - -#[tokio::test] -async fn scan_redirect_vlt_artifact_sha512_mismatch() { - assert_preflight_refuses( - ResponseTemplate::new(200).set_body_bytes(b"other bytes".to_vec()), - "sha512 mismatch", - ) - .await; -} - -#[tokio::test] -async fn scan_redirect_vlt_artifact_http_404() { - assert_preflight_refuses(ResponseTemplate::new(404), "http 404").await; -} - -#[tokio::test] -async fn scan_redirect_vlt_artifact_http_500() { - assert_preflight_refuses(ResponseTemplate::new(500), "http 500").await; -} - -#[tokio::test] -async fn scan_redirect_vlt_artifact_fetch_error() { - let server = MockServer::start().await; - mock_discovery(&server).await; - let url = format!("http://127.0.0.1:9{}", artifact_path()); - mock_reference_at(&server, &url, &sha512_sri(&patched_tarball())).await; - mock_view(&server).await; - let tmp = tempfile::tempdir().unwrap(); - write_vlt_project(tmp.path(), Era::V1); - let lock = read(tmp.path(), "vlt-lock.json"); - - let (_, doc) = scan_hosted(tmp.path(), &server, &[], &[]); - - assert_eq!(redirected(&doc), 0); - assert_eq!(read(tmp.path(), "vlt-lock.json"), lock); - let detail = warning_detail(&doc, UNVERIFIABLE); - let redacted = url.replace(&format!("/{TOKEN}/"), "//"); - assert!( - detail.starts_with(&format!( - "vlt would fail to verify {redacted}: fetch error " - )) && detail.ends_with(&format!("; nothing was written for {PURL}")), - "the fetch-error refusal quotes the redacted URL" - ); - assert!( - !detail.contains(TOKEN), - "the grant token never reaches the warning" - ); -} - -async fn redirect_chain(hops: usize) -> (Value, tempfile::TempDir) { - let server = MockServer::start().await; - mock_discovery(&server).await; - mock_view(&server).await; - for hop in 0..hops { - Mock::given(method("GET")) - .and(path(format!("/hop/{hop}"))) - .respond_with( - ResponseTemplate::new(302) - .insert_header("location", format!("{}/hop/{}", server.uri(), hop + 1)), - ) - .mount(&server) - .await; - } - Mock::given(method("GET")) - .and(path(format!("/hop/{hops}"))) - .respond_with(ResponseTemplate::new(200).set_body_bytes(patched_tarball())) - .mount(&server) - .await; - mock_reference_at( - &server, - &format!("{}/hop/0", server.uri()), - &sha512_sri(&patched_tarball()), - ) - .await; - let tmp = tempfile::tempdir().unwrap(); - write_vlt_project(tmp.path(), Era::V1); - let (_, doc) = scan_hosted(tmp.path(), &server, &[], &[]); - (doc, tmp) -} - -#[tokio::test] -async fn scan_redirect_vlt_artifact_redirect_chain_10_passes() { - let (doc, _tmp) = redirect_chain(10).await; - assert_eq!(redirected(&doc), 1, "{doc:#}"); -} - -#[tokio::test] -async fn scan_redirect_vlt_artifact_redirect_chain_11_fails() { - let (doc, _tmp) = redirect_chain(11).await; - assert_eq!(redirected(&doc), 0); - assert!(warning_detail(&doc, UNVERIFIABLE).contains(": fetch error ")); -} - -#[tokio::test] -async fn scan_redirect_vlt_artifact_one_get_per_distinct_url() { - let server = MockServer::start().await; - mock_all(&server).await; - let tmp = tempfile::tempdir().unwrap(); - write_vlt_project(tmp.path(), Era::V1); - let ids = [ - TILDE_ID, - "~npm~left-pad@1.3.0~peer.2", - "~npm~left-pad@1.3.0~peer.3", - ]; - let nodes: Vec = ids.iter().map(|id| registry_node(id)).collect(); - std::fs::write(tmp.path().join("vlt-lock.json"), lock_with(Era::V1, &nodes)).unwrap(); - - let (_, doc) = scan_hosted(tmp.path(), &server, &["--dry-run"], &[]); - assert_eq!(redirected(&doc), 1); - assert_eq!(artifact_requests(&server).await, 1); -} - -#[tokio::test] -async fn scan_redirect_vlt_artifact_dry_run_still_preflights_writes_nothing() { - let server = MockServer::start().await; - mock_discovery(&server).await; - mock_reference(&server).await; - mock_artifact_with( - &server, - ResponseTemplate::new(200) - .insert_header("content-encoding", "gzip") - .set_body_bytes(gzip(&patched_tarball())), - ) - .await; - let tmp = tempfile::tempdir().unwrap(); - write_vlt_project(tmp.path(), Era::V1); - let lock = read(tmp.path(), "vlt-lock.json"); - - let (_, doc) = scan_hosted(tmp.path(), &server, &["--dry-run"], &[]); - - assert_eq!(artifact_requests(&server).await, 1); - assert_eq!(redirected(&doc), 0); - assert!(warning_codes(&doc).contains(&UNVERIFIABLE.to_string())); - assert_eq!(read(tmp.path(), "vlt-lock.json"), lock); - assert!(!tmp.path().join(".socket").exists()); -} +// ── no artifact fetch ──────────────────────────────────────────────────── async fn gzip_artifact_server() -> MockServer { let server = MockServer::start().await; @@ -426,99 +226,36 @@ async fn gzip_artifact_server() -> MockServer { server } +/// The scan pins the hosted artifact without downloading it, dry or wet, +/// even from a server that would content-encode it: the patch server sends +/// archives with `Cache-Control: no-transform`, and `vlt ci` verifies the +/// lock's sha512 against the bytes it receives. #[tokio::test] -async fn scan_redirect_vlt_artifact_failed_dep_withheld_from_every_rewriter() { - let server = gzip_artifact_server().await; - let tmp = tempfile::tempdir().unwrap(); - write_installed_vlt_project(tmp.path(), PRISTINE); - std::fs::write(tmp.path().join("package-lock.json"), package_lock()).unwrap(); - - let (_, doc) = scan_hosted(tmp.path(), &server, &[], &[]); - - assert_eq!(redirected(&doc), 0); - assert_eq!(read(tmp.path(), "package-lock.json"), package_lock()); - assert_eq!( - read(tmp.path(), "vlt-lock.json"), - lock_with(Era::V1, &[registry_node(TILDE_ID)]) - ); - assert!(store_dir(tmp.path(), TILDE_ID).exists()); -} - -#[tokio::test] -async fn scan_redirect_vlt_artifact_ambiguous_withholds_vlt_only() { - let server = gzip_artifact_server().await; - let tmp = tempfile::tempdir().unwrap(); - write_vlt_project(tmp.path(), Era::V1); - std::fs::write(tmp.path().join("package-lock.json"), package_lock()).unwrap(); - - let (_, doc) = scan_hosted(tmp.path(), &server, &["--no-npm-allow-remote-config"], &[]); - - assert_eq!( - redirected(&doc), - 1, - "package-lock.json still confirms: {doc:#}" - ); - assert!(read(tmp.path(), "package-lock.json").contains(&artifact_url(&server))); - assert_eq!( - read(tmp.path(), "vlt-lock.json"), - lock_with(Era::V1, &[registry_node(TILDE_ID)]) - ); - assert_eq!( - warning_detail(&doc, UNVERIFIABLE), - format!( - "vlt would fail to verify {}: content-encoding gzip; vlt-lock.json was not changed \ - for {PURL}", - redacted_artifact_url(&server) - ) - ); - assert!(skipped_reasons(&doc).is_empty(), "{doc:#}"); -} - -/// A package-lock.json that lists only the root: the npm rewriter has -/// nothing to pin. -fn package_lock_without_dep() -> String { - r#"{ - "name": "consumer", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { "name": "consumer", "version": "0.0.0" } - } -} -"# - .to_string() -} - -#[tokio::test] -async fn scan_redirect_vlt_artifact_ambiguous_earlier_pin_is_not_confirmed() { - let server = gzip_artifact_server().await; - let tmp = tempfile::tempdir().unwrap(); - write_vlt_project(tmp.path(), Era::V1); - let pinned = lock_with(Era::V1, &[pinned_node(TILDE_ID, &server)]); - std::fs::write(tmp.path().join("vlt-lock.json"), &pinned).unwrap(); - std::fs::write( - tmp.path().join("package-lock.json"), - package_lock_without_dep(), - ) - .unwrap(); +async fn scan_redirect_vlt_pins_without_fetching_the_artifact() { + for dry_run in [false, true] { + let server = gzip_artifact_server().await; + let tmp = tempfile::tempdir().unwrap(); + write_vlt_project(tmp.path(), Era::V1); + let lock = read(tmp.path(), "vlt-lock.json"); + let extra: &[&str] = if dry_run { &["--dry-run"] } else { &[] }; - let (_, doc) = scan_hosted(tmp.path(), &server, &["--no-npm-allow-remote-config"], &[]); + let (_, doc) = scan_hosted(tmp.path(), &server, extra, &[]); - assert_eq!( - redirected(&doc), - 0, - "the earlier run's vlt pin alone confirms nothing: {doc:#}" - ); - assert_eq!(read(tmp.path(), "vlt-lock.json"), pinned); - assert_eq!( - warning_detail(&doc, UNVERIFIABLE), - format!( - "vlt would fail to verify {}: content-encoding gzip; {PURL} was left pinned by an \ - earlier run and `vlt ci` will fail until the artifact verifies", - redacted_artifact_url(&server) - ) - ); + assert_eq!(redirected(&doc), 1, "dry_run={dry_run}: {doc:#}"); + assert_eq!(artifact_requests(&server).await, 0, "dry_run={dry_run}"); + assert!( + skipped_reasons(&doc).is_empty(), + "dry_run={dry_run}: {doc:#}" + ); + if dry_run { + assert_eq!(read(tmp.path(), "vlt-lock.json"), lock); + } else { + assert_eq!( + read(tmp.path(), "vlt-lock.json"), + lock_with(Era::V1, &[pinned_node(TILDE_ID, &server)]) + ); + } + } } /// REGRESSION (#471): `bund` bundles left-pad@1.3.0, which vlt unpacks @@ -569,11 +306,12 @@ async fn scan_redirect_vlt_bundled_store_copy_warns() { /// A real `node_modules/.vlt` store with no hidden lock (0.0.0-1 and /// 0.0.0-32 write none) is vlt's install state too: vlt drives beside a -/// package-lock.json, so there is no sibling warning and a failed preflight -/// withholds the dep from every rewriter. +/// package-lock.json, so there is no sibling warning and the vlt pin +/// confirms. #[tokio::test] async fn scan_redirect_vlt_store_dir_without_hidden_lock_drives() { - let server = gzip_artifact_server().await; + let server = MockServer::start().await; + mock_all(&server).await; let tmp = tempfile::tempdir().unwrap(); write_vlt_project(tmp.path(), Era::V1); install_store(tmp.path(), TILDE_ID, PRISTINE); @@ -581,19 +319,14 @@ async fn scan_redirect_vlt_store_dir_without_hidden_lock_drives() { let (_, doc) = scan_hosted(tmp.path(), &server, &["--no-npm-allow-remote-config"], &[]); - assert_eq!(redirected(&doc), 0, "{doc:#}"); + assert_eq!(redirected(&doc), 1, "{doc:#}"); assert!( !warning_codes(&doc).contains(&"redirect_vlt_sibling_lockfiles".to_string()), "{doc:#}" ); - assert_eq!(skipped_reasons(&doc), [UNVERIFIABLE]); - assert_eq!(read(tmp.path(), "package-lock.json"), package_lock()); assert_eq!( - warning_detail(&doc, UNVERIFIABLE), - format!( - "vlt would fail to verify {}: content-encoding gzip; nothing was written for {PURL}", - redacted_artifact_url(&server) - ) + read(tmp.path(), "vlt-lock.json"), + lock_with(Era::V1, &[pinned_node(TILDE_ID, &server)]) ); } @@ -686,106 +419,11 @@ async fn scan_redirect_vlt_not_driving_refused_dep_is_not_confirmed_by_the_sibli assert_no_ledger(tmp.path()); } -const VENDORED_UUID: &str = "11111111-2222-4333-8444-555555555555"; - -/// A vlt project whose left-pad is vendored (§3.4 D19 dir node) and -/// claimed by a `flavor: "vlt"` vendored ledger entry. -fn write_vlt_vendored_project(root: &Path) -> (String, Vec) { - write_package_json(root); - install_importer(root, PATCHED); - let dir = format!(".socket/vendor/npm/{VENDORED_UUID}/{NAME}-{VERSION}/node_modules/{NAME}"); - let lock = lock_with( - Era::V1, - &[format!( - "\"file~.socket+vendor+npm+{VENDORED_UUID}+{NAME}-{VERSION}+node__modules+{NAME}\": \ - [0,\"{NAME}\",null,\"{dir}\"]" - )], - ); - std::fs::write(root.join("vlt-lock.json"), &lock).unwrap(); - let state = serde_json::json!({ - "version": 1, - "entries": { PURL: { - "ecosystem": "npm", - "basePurl": PURL, - "uuid": VENDORED_UUID, - "artifact": { "path": format!(".socket/vendor/npm/{VENDORED_UUID}/{NAME}-{VERSION}") }, - "wiring": [], - "flavor": "vlt" - }} - }); - std::fs::create_dir_all(root.join(".socket/vendor")).unwrap(); - let state = serde_json::to_vec_pretty(&state).unwrap(); - std::fs::write(root.join(".socket/vendor/state.json"), &state).unwrap(); - (lock, state) -} - -/// Vendored → hosted (§4.10): the takeover restores the registry node this -/// run pins, so the artifact is probed through the vendored node BEFORE the -/// revert; a failure keeps the package vendored (never reverted), with or -/// without another npm-family lock, wet or dry. -#[tokio::test] -async fn scan_redirect_vlt_vendored_takeover_preflights_before_the_revert() { - for (sibling, dry_run) in [(false, false), (false, true), (true, false)] { - let server = gzip_artifact_server().await; - let tmp = tempfile::tempdir().unwrap(); - let (lock, state) = write_vlt_vendored_project(tmp.path()); - if sibling { - std::fs::write(tmp.path().join("package-lock.json"), package_lock()).unwrap(); - } - let mut extra = vec!["--no-npm-allow-remote-config"]; - if dry_run { - extra.push("--dry-run"); - } - - let (_, doc) = scan_hosted(tmp.path(), &server, &extra, &[]); - - let leg = format!("sibling={sibling} dry_run={dry_run}: {doc:#}"); - assert_eq!(artifact_requests(&server).await, 1, "{leg}"); - assert_eq!(redirected(&doc), 0, "{leg}"); - assert_eq!(skipped_reasons(&doc), [UNVERIFIABLE], "{leg}"); - assert!( - !warning_codes(&doc) - .iter() - .any(|c| c.contains("revert") || c.contains("takeover")), - "no takeover is attempted: {leg}" - ); - assert_eq!(read(tmp.path(), "vlt-lock.json"), lock, "{leg}"); - assert_eq!( - std::fs::read(tmp.path().join(".socket/vendor/state.json")).unwrap(), - state, - "{leg}" - ); - if sibling { - assert_eq!( - read(tmp.path(), "package-lock.json"), - package_lock(), - "{leg}" - ); - } - } - - let server = MockServer::start().await; - mock_all(&server).await; - let tmp = tempfile::tempdir().unwrap(); - write_vlt_vendored_project(tmp.path()); - let (_, doc) = scan_hosted(tmp.path(), &server, &["--dry-run"], &[]); - assert_eq!( - artifact_requests(&server).await, - 1, - "a verifying artifact is probed too: {doc:#}" - ); - assert!( - !warning_codes(&doc).contains(&UNVERIFIABLE.to_string()), - "{doc:#}" - ); -} - #[tokio::test] -async fn scan_redirect_vlt_artifact_get_uuid_driver() { +async fn get_uuid_hosted_pins_vlt_without_fetching_the_artifact() { let server = gzip_artifact_server().await; let tmp = tempfile::tempdir().unwrap(); write_vlt_project(tmp.path(), Era::V1); - let lock = read(tmp.path(), "vlt-lock.json"); let cwd = tmp.path().to_str().unwrap().to_string(); let uri = server.uri(); @@ -810,130 +448,14 @@ async fn scan_redirect_vlt_artifact_get_uuid_driver() { ); assert_eq!(code, 0, "{doc:#}\n{stderr}"); - assert_eq!(artifact_requests(&server).await, 1); - assert_eq!(redirected(&doc), 0); - assert!(warning_codes(&doc).contains(&UNVERIFIABLE.to_string())); - assert_eq!(read(tmp.path(), "vlt-lock.json"), lock); -} - -fn pnpm_lock() -> String { - format!( - "lockfileVersion: '9.0'\n\nimporters:\n .:\n dependencies:\n {NAME}:\n \ - specifier: {VERSION}\n version: {VERSION}\n\npackages:\n {NAME}@{VERSION}:\n \ - resolution: {{integrity: {UPSTREAM_SHA512}}}\n\nsnapshots:\n {NAME}@{VERSION}: {{}}\n" - ) -} - -fn bun_lock() -> String { - format!( - "{{\n \"lockfileVersion\": 1,\n \"packages\": {{\n \"{NAME}\": [\"{NAME}@{VERSION}\", \ - \"\", {{}}, \"{UPSTREAM_SHA512}\"],\n }}\n}}\n" - ) -} - -#[tokio::test] -async fn scan_redirect_vlt_artifact_no_preflight_without_vlt_lock() { - for (lock, text) in [ - ("package-lock.json", package_lock()), - ("pnpm-lock.yaml", pnpm_lock()), - ("bun.lock", bun_lock()), - ] { - let server = MockServer::start().await; - mock_all(&server).await; - let tmp = tempfile::tempdir().unwrap(); - write_package_json(tmp.path()); - install_importer(tmp.path(), PRISTINE); - std::fs::write(tmp.path().join(lock), text).unwrap(); - - let (_, doc) = scan_hosted(tmp.path(), &server, &["--no-npm-allow-remote-config"], &[]); - - assert_eq!(redirected(&doc), 1, "{lock}: {doc:#}"); - assert!( - read(tmp.path(), lock).contains(&artifact_url(&server)), - "{lock}" - ); - assert_eq!(artifact_requests(&server).await, 0, "{lock}"); - assert!( - !warning_codes(&doc) - .iter() - .any(|c| c.starts_with("redirect_vlt_")), - "{lock}: {doc:#}" - ); - } -} - -#[tokio::test] -async fn scan_redirect_vlt_artifact_proxy_honored() { - let server = MockServer::start().await; - mock_discovery(&server).await; - mock_view(&server).await; - mock_artifact(&server).await; - let url = format!("http://vlt-artifact.invalid{}", artifact_path()); - mock_reference_at(&server, &url, &sha512_sri(&patched_tarball())).await; - let tmp = tempfile::tempdir().unwrap(); - write_vlt_project(tmp.path(), Era::V1); - let proxy = server.uri(); - - let (_, doc) = scan_hosted( - tmp.path(), - &server, - &[], - &[ - ("HTTP_PROXY", proxy.as_str()), - ("http_proxy", proxy.as_str()), - ("NO_PROXY", "127.0.0.1,localhost"), - ("no_proxy", "127.0.0.1,localhost"), - ], - ); - + assert_eq!(artifact_requests(&server).await, 0); assert_eq!(redirected(&doc), 1, "{doc:#}"); - assert_eq!(artifact_requests(&server).await, 1); -} - -#[tokio::test] -async fn scan_redirect_vlt_artifact_already_pinned_failure_left_pinned() { - let server = gzip_artifact_server().await; - let tmp = tempfile::tempdir().unwrap(); - write_vlt_project(tmp.path(), Era::V1); - let pinned = lock_with(Era::V1, &[pinned_node(TILDE_ID, &server)]); - std::fs::write(tmp.path().join("vlt-lock.json"), &pinned).unwrap(); - - let (_, doc) = scan_hosted(tmp.path(), &server, &[], &[]); - - assert_eq!(redirected(&doc), 0, "neither confirmed nor attested"); assert_eq!( read(tmp.path(), "vlt-lock.json"), - pinned, - "nothing is reverted" - ); - assert_eq!( - warning_detail(&doc, UNVERIFIABLE), - format!( - "vlt would fail to verify {}: content-encoding gzip; {PURL} was left pinned by an \ - earlier run and `vlt ci` will fail until the artifact verifies", - redacted_artifact_url(&server) - ) + lock_with(Era::V1, &[pinned_node(TILDE_ID, &server)]) ); } -#[tokio::test] -async fn scan_redirect_vlt_artifact_no_bearer_on_artifact_request() { - let server = MockServer::start().await; - mock_all(&server).await; - let tmp = tempfile::tempdir().unwrap(); - write_vlt_project(tmp.path(), Era::V1); - - scan_hosted(tmp.path(), &server, &[], &[]); - - let requests = server.received_requests().await.unwrap(); - let (artifact, api): (Vec<_>, Vec<_>) = requests - .iter() - .partition(|r| r.url.path() == artifact_path()); - assert_eq!(artifact.len(), 1); - assert!(artifact[0].headers.get("authorization").is_none()); - assert!(api.iter().any(|r| r.headers.get("authorization").is_some())); -} - // ── warm-tree heal ─────────────────────────────────────────────────────── #[tokio::test] @@ -981,8 +503,10 @@ async fn scan_redirect_vlt_heal_rule_c_no_hidden_lock_with_record() { assert_eq!(warning_detail(&doc, ADVISORY), advisory_invalidated(1)); } +/// Without a hidden lock or a patch record, the installed copy cannot be +/// judged: it is kept, and the advisory says to run `vlt ci`. #[tokio::test] -async fn scan_redirect_vlt_heal_rule_c_no_record_uses_artifact_bytes() { +async fn scan_redirect_vlt_heal_rule_c_no_record_is_undeterminable() { let server = MockServer::start().await; mock_discovery(&server).await; mock_reference(&server).await; @@ -995,21 +519,12 @@ async fn scan_redirect_vlt_heal_rule_c_no_record_uses_artifact_bytes() { let tmp = tempfile::tempdir().unwrap(); write_installed_vlt_project(tmp.path(), PRISTINE); std::fs::remove_file(tmp.path().join("node_modules/.vlt-lock.json")).unwrap(); - let healthy = tempfile::tempdir().unwrap(); - write_installed_vlt_project(healthy.path(), PATCHED); - std::fs::remove_file(healthy.path().join("node_modules/.vlt-lock.json")).unwrap(); let (_, doc) = scan_hosted(tmp.path(), &server, &[], &[]); - let (_, healthy_doc) = scan_hosted(healthy.path(), &server, &[], &[]); assert!(warning_codes(&doc).contains(&"record_fetch_failed".to_string())); - assert!(!store_dir(tmp.path(), TILDE_ID).exists()); - assert_eq!(warning_detail(&doc, ADVISORY), advisory_invalidated(1)); - assert!(store_dir(healthy.path(), TILDE_ID).exists()); - assert_eq!( - warning_detail(&healthy_doc, ADVISORY), - ADVISORY_NOTHING_STALE - ); + assert!(store_dir(tmp.path(), TILDE_ID).exists()); + assert_eq!(warning_detail(&doc, ADVISORY), advisory_undeterminable(1)); } /// A warm install of `nodes` (each `(DepID, flags)`) holding `index`, with diff --git a/crates/socket-patch-cli/tests/in_process_vendor_bun_takeover/vlt.rs b/crates/socket-patch-cli/tests/in_process_vendor_bun_takeover/vlt.rs index 63b7dedb0..c8c0a227d 100644 --- a/crates/socket-patch-cli/tests/in_process_vendor_bun_takeover/vlt.rs +++ b/crates/socket-patch-cli/tests/in_process_vendor_bun_takeover/vlt.rs @@ -11,8 +11,7 @@ //! the hosted rewrite, and `rollback` (the upstream restore) lands on //! the registry lock; //! 3. refusals fire BEFORE the other mode is reverted: the complete vlt -//! vendored preflight in front of a hosted revert, and the hosted -//! artifact preflight in front of a vendored revert. +//! vendored preflight in front of a hosted revert. #[path = "../prebuilt_common/mod.rs"] mod prebuilt_common; @@ -471,42 +470,6 @@ async fn vlt_vendored_preflight_refuses_before_the_hosted_revert() { assert_intact("scan --mode vendored --dry-run"); } -#[tokio::test(flavor = "multi_thread")] -async fn vlt_hosted_artifact_preflight_refuses_before_the_vendored_revert() { - let tmp = tempfile::tempdir().unwrap(); - let root = tmp.path(); - write_project(root); - seed_manifest(root); - let (code, env, _) = vendor(root, &[]); - assert_eq!(code, 0, "{env:#}"); - std::fs::remove_file(root.join(".socket/manifest.json")).unwrap(); - let lock = hosted::read(root, "vlt-lock.json"); - let pkg = hosted::read(root, "package.json"); - let state = std::fs::read(root.join(".socket/vendor/state.json")).unwrap(); - - let server = MockServer::start().await; - hosted::mock_discovery(&server).await; - hosted::mock_reference(&server).await; - hosted::mock_view(&server).await; - hosted::mock_artifact_with(&server, ResponseTemplate::new(404)).await; - let (_, env, stderr) = scan(root, &server, "hosted", &[]); - assert!( - hosted::warning_codes(&env).contains(&hosted::UNVERIFIABLE.to_string()), - "{env:#}\n{stderr}" - ); - assert_eq!( - hosted::read(root, "vlt-lock.json"), - lock, - "the vendored wiring stays" - ); - assert_eq!(hosted::read(root, "package.json"), pkg); - assert_eq!( - std::fs::read(root.join(".socket/vendor/state.json")).unwrap(), - state - ); - assert!(root.join(rel()).join("index.js").is_file()); -} - /// A vendor that fails after the takeover's upstream restore (here a /// patch-service artifact failing its integrity check) rolls the restore /// back (#853, #944): the purl stays hosted-patched, the lock and the diff --git a/crates/socket-patch-cli/tests/scan/hosted_symlinked_files.rs b/crates/socket-patch-cli/tests/scan/hosted_symlinked_files.rs index 8e30f0459..7da9741bf 100644 --- a/crates/socket-patch-cli/tests/scan/hosted_symlinked_files.rs +++ b/crates/socket-patch-cli/tests/scan/hosted_symlinked_files.rs @@ -576,8 +576,8 @@ async fn hosted_refuses_symlinked_vlt_lock_before_ledger_write() { assert_refused_untouched(code, &doc, &root, "vlt-lock.json", &target, &lock_before); } -/// A FIFO planted as `vlt-lock.json` is skipped by the preflight read and -/// the candidate read alike: the scan returns and never probes an artifact. +/// A FIFO planted as `vlt-lock.json` is skipped by the candidate read: the +/// scan returns and never fetches an artifact. #[tokio::test] async fn hosted_scan_returns_with_fifo_vlt_lock() { use vlt_hosted_common as vlt; diff --git a/crates/socket-patch-cli/tests/vlt-leg-manifest.json b/crates/socket-patch-cli/tests/vlt-leg-manifest.json index 6c84acb9f..a02a57310 100644 --- a/crates/socket-patch-cli/tests/vlt-leg-manifest.json +++ b/crates/socket-patch-cli/tests/vlt-leg-manifest.json @@ -32,7 +32,6 @@ "default_registry_alias", "registry_from_env", "registry_from_user_config", - "content_encoding_refused", "old_lockfile_ignored", "warm_cache_hazard", "idempotence", @@ -168,7 +167,6 @@ "default_registry_alias", "registry_from_env", "registry_from_user_config", - "content_encoding_refused", "old_lockfile_ignored", "warm_cache_hazard", "idempotence", diff --git a/crates/socket-patch-cli/tests/vlt_e2e_common/fixture.rs b/crates/socket-patch-cli/tests/vlt_e2e_common/fixture.rs index 0865d061d..5463e314d 100644 --- a/crates/socket-patch-cli/tests/vlt_e2e_common/fixture.rs +++ b/crates/socket-patch-cli/tests/vlt_e2e_common/fixture.rs @@ -22,7 +22,6 @@ pub const UUID_USX: &str = "d4d4d4d4-4444-4444-8444-444444444444"; pub const UUID_FSEVENTS: &str = "e5e5e5e5-5555-4555-8555-555555555555"; pub const ADVISORY: &str = "redirect_vlt_reinstall_required"; -pub const UNVERIFIABLE: &str = "redirect_vlt_artifact_unverifiable"; pub const OPTIONAL_KEPT: &str = "socket-patch does not remove them because `vlt install` does not \ reinstall a removed optional dependency. Run `vlt ci` (or delete node_modules and run `vlt \ install`). vlt 0.0.0-30 … 1.0.4 install no optional dependency from the lock of a \ @@ -44,6 +43,14 @@ pub fn advisory_invalidated(n: usize) -> String { format!("{}{VLT_UPDATE_NOTE}", invalidated_head(n)) } +pub fn advisory_undeterminable(n: usize) -> String { + format!( + "vlt-lock.json pins Socket-patched packages, but socket-patch could not check {n} \ + installed copies (node_modules is a link, or no patch record was available); run \ + `vlt ci` to be sure the patched packages are installed." + ) +} + pub fn advisory_cleanup_skipped(n: usize) -> String { format!( "vlt-lock.json pins Socket-patched packages, but node_modules still holds {n} unpatched \ diff --git a/crates/socket-patch-cli/tests/vlt_e2e_common/mod.rs b/crates/socket-patch-cli/tests/vlt_e2e_common/mod.rs index d6970c665..fc6f87c58 100644 --- a/crates/socket-patch-cli/tests/vlt_e2e_common/mod.rs +++ b/crates/socket-patch-cli/tests/vlt_e2e_common/mod.rs @@ -1705,24 +1705,6 @@ impl PatchService { .await; } - /// Serve `t`'s artifact gzip-encoded, as patch.socket.dev did before - /// the `no-transform` fix. - pub async fn serve_artifact_gzip_encoded(&self, t: &PatchTarget) { - let mut enc = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default()); - enc.write_all(&t.tgz).unwrap(); - Mock::given(method("GET")) - .and(path(t.artifact_path())) - .respond_with( - ResponseTemplate::new(200) - .insert_header("content-type", "application/octet-stream") - .insert_header("content-encoding", "gzip") - .set_body_bytes(enc.finish().unwrap()), - ) - .with_priority(1) - .mount(&self.server) - .await; - } - pub async fn artifact_hits(&self, t: &PatchTarget) -> usize { self.server .received_requests() diff --git a/crates/socket-patch-cli/tests/vlt_hosted_common/mod.rs b/crates/socket-patch-cli/tests/vlt_hosted_common/mod.rs index 458839ce6..999c27ca3 100644 --- a/crates/socket-patch-cli/tests/vlt_hosted_common/mod.rs +++ b/crates/socket-patch-cli/tests/vlt_hosted_common/mod.rs @@ -1,9 +1,9 @@ //! Hermetic vlt hosted-mode fixtures shared by the redirect, rollback, //! symlink, get and covgap suites: a vlt project (`vlt-lock.json`, an //! installed importer copy, optionally vlt's store and hidden lock), the -//! wiremock API (discovery, the grant, the patch view) plus the artifact -//! route the vlt preflight fetches, and a scrubbed subprocess runner that -//! reads the `--json` envelope back. +//! wiremock API (discovery, the grant, the patch view) plus an artifact +//! route (counted, so a suite can assert the scan never fetches it), and a +//! scrubbed subprocess runner that reads the `--json` envelope back. //! //! Include with `#[path = "vlt_hosted_common/mod.rs"] mod vlt_hosted_common;`. @@ -35,7 +35,6 @@ pub const LEGACY_ID: &str = "·npm·left-pad@1.3.0"; pub const EMPTY_SEGMENT_ID: &str = "··left-pad@1.3.0"; pub const ADVISORY: &str = "redirect_vlt_reinstall_required"; -pub const UNVERIFIABLE: &str = "redirect_vlt_artifact_unverifiable"; pub const ADVISORY_NOTHING_STALE: &str = "vlt-lock.json pins Socket-patched packages; fresh \ checkouts install them with `vlt ci` or `vlt install --frozen-lockfile`. Note: `vlt update` \ @@ -61,8 +60,8 @@ pub fn advisory_cleanup_skipped(n: usize) -> String { pub fn advisory_undeterminable(n: usize) -> String { format!( "vlt-lock.json pins Socket-patched packages, but socket-patch could not check {n} \ - installed copies (node_modules is a link, or no patch record or artifact was \ - available); run `vlt ci` to be sure the patched packages are installed." + installed copies (node_modules is a link, or no patch record was available); run \ + `vlt ci` to be sure the patched packages are installed." ) } diff --git a/crates/socket-patch-core/src/api/client.rs b/crates/socket-patch-core/src/api/client.rs index b22f78291..555e6a97b 100644 --- a/crates/socket-patch-core/src/api/client.rs +++ b/crates/socket-patch-core/src/api/client.rs @@ -509,12 +509,6 @@ impl ApiClient { self } - /// The User-Agent-only client for grant-tokenized artifact URLs, where - /// the Socket bearer must never be sent. - pub(crate) fn plain_http(&self) -> &reqwest::Client { - &self.plain - } - /// The run-level breaker's consecutive-failure count (tests). #[cfg(test)] pub(crate) fn vendor_outage_count(&self) -> u32 { diff --git a/crates/socket-patch-core/src/hosted/engine.rs b/crates/socket-patch-core/src/hosted/engine.rs index 09901663b..063bfc740 100644 --- a/crates/socket-patch-core/src/hosted/engine.rs +++ b/crates/socket-patch-core/src/hosted/engine.rs @@ -7,26 +7,23 @@ //! //! 1. [`build_candidates`] — reference grants → rewriter overrides. //! 2. [`bun_lockb_symlinked`] — the binary-lock symlink refusal. -//! 3. vlt artifact preflight ([`super::vlt`]) + [`withhold_everywhere`]. -//! 4. (caller) the apply lock and the vendored→hosted takeover. -//! 5. [`read_candidate_files`] → [`wheel_targets`] → (caller) wheel metadata, +//! 3. (caller) the apply lock and the vendored→hosted takeover. +//! 4. [`read_candidate_files`] → [`wheel_targets`] → (caller) wheel metadata, //! and [`yarn_berry_manifest_targets`] → (caller) served npm manifests. -//! 6. [`rewrite`] — the rewriters, the pnpm `trustLockfile` and npm +//! 5. [`rewrite`] — the rewriters, the pnpm `trustLockfile` and npm //! `allow-remote` auto-configs, and the per-ecosystem confirmation. -//! 7. [`guard`] — the symlink / unreadable-file refusal before any write. +//! 6. [`guard`] — the symlink / unreadable-file refusal before any write. //! //! Nothing here writes, spawns, reads the environment or touches the -//! network: every host effect (locking, probes, record fetches, the commit -//! of the rewritten files) stays with the caller. +//! network: every host effect (locking, record fetches, the commit of the +//! rewritten files) stays with the caller. use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use serde::{Deserialize, Serialize}; use crate::api::types::PackageVendorResult; -use crate::constants::npm_family::{ - RUSH_COMMON_LOCK_REL, RUSH_SUBSPACES_DIR, VLT_HIDDEN_LOCK_REL, VLT_LOCK, -}; +use crate::constants::npm_family::{RUSH_COMMON_LOCK_REL, RUSH_SUBSPACES_DIR, VLT_HIDDEN_LOCK_REL}; use crate::formats::cargo::source_replacement::CratesIoSource; use crate::formats::governing_locks::PYPI_REQUIREMENTS; use crate::patch::redirect::npmrc::{ @@ -37,7 +34,7 @@ use crate::patch::redirect::npmrc::{ use crate::patch::redirect::presence::groups_present; use crate::patch::redirect::yarnrc::OuterYarnMirror; use crate::patch::redirect::{ - artifact_url_spellings, rewrite_registry_redirect_withholding_vlt, DepOverride, FileEdit, + artifact_url_spellings, rewrite_registry_redirect_with_host, DepOverride, FileEdit, RewriteResult, RewriteWarning, }; use crate::utils::pnpm_workspace::governing_workspace_file; @@ -321,22 +318,6 @@ pub fn bun_lockb_symlinked(view: &ProjectView<'_>, candidates: &[Candidate]) -> && view.is_symlink("bun.lockb") } -/// Drop the deps the vlt preflight withheld from every rewriter, recording -/// each as skipped. -pub fn withhold_everywhere( - candidates: &mut Vec, - withheld_everywhere: &BTreeMap, - skipped: &mut Vec, -) { - if withheld_everywhere.is_empty() { - return; - } - for (uuid, purl) in withheld_everywhere { - skipped.push(SkippedPatch::new(purl, uuid, super::vlt::WITHHELD_REASON)); - } - candidates.retain(|c| !withheld_everywhere.contains_key(&c.dep.patch_uuid)); -} - /// The project's candidate files as the rewriters read them. #[derive(Debug, Clone, Default)] pub struct CandidateFiles { @@ -1888,9 +1869,6 @@ fn rush_repo_state_present(view: &ProjectView<'_>, rel: &str) -> bool { enum ProbeStep { Decided(bool), Needles(Vec), - /// [`Self::Needles`], searched in every final text but `vlt-lock.json` - /// (a dep withheld from the vlt rewrite). - NeedlesOutsideVlt(Vec), } /// The substrings whose presence in a final text confirms `dep`'s redirect — @@ -1923,15 +1901,12 @@ pub fn candidate_presence_needles(dep: &DepOverride) -> Vec { /// Rewrite the candidate files for `candidates`, plan the install-policy /// auto-configs, and confirm which redirects the final files pin. /// -/// `python_metadata` maps a wheel's artifact URL to its fetched METADATA; -/// `withheld_from_vlt` are the uuids the vlt preflight kept out of the vlt -/// rewrite. +/// `python_metadata` maps a wheel's artifact URL to its fetched METADATA. pub async fn rewrite( view: &ProjectView<'_>, read: CandidateFiles, candidates: &[Candidate], python_metadata: BTreeMap, - withheld_from_vlt: &BTreeSet, options: RewriteOptions<'_>, ) -> Rewritten { // The run must never leave wiring that lockfile discovery — what `vex`, @@ -1943,10 +1918,7 @@ pub async fn rewrite( // least one candidate, so this ends. A staged takeover is never dropped // (see [`RewriteOptions::takeover_uuids`]): its vendored wiring is // reverted in the overlay, and its pin keeps the rewriters' verdict. - let exempt: BTreeSet = withheld_from_vlt - .union(&options.takeover_uuids) - .cloned() - .collect(); + let exempt: BTreeSet = options.takeover_uuids.clone(); let mut kept: Vec = candidates.to_vec(); let mut unattributed: Vec = Vec::new(); loop { @@ -1955,7 +1927,6 @@ pub async fn rewrite( read.clone(), &kept, python_metadata.clone(), - withheld_from_vlt, options.clone(), ) .await; @@ -2045,9 +2016,8 @@ async fn constraints_link_warnings( /// bundled or user-patched copy the rewriters knowingly left on the /// registry (`bundled_skipped_uuids`, a yarn `npm:` alias entry in /// `alias_skipped_entries`, or a bundled copy in vlt's store; all are -/// warned and kept out of the in-run VEX), or one withheld from -/// the vlt rewrite while a sibling lock takes it, and a wet vendored→hosted -/// takeover whose vendored wiring the caller already reverted (both in +/// warned and kept out of the in-run VEX), and a wet vendored→hosted +/// takeover whose vendored wiring the caller already reverted (in /// `exempt`). Which unreachable copies should block a redirect is the /// copy-source policy (audit B16), not decided here. /// @@ -2364,7 +2334,6 @@ async fn rewrite_once( read: CandidateFiles, candidates: &[Candidate], python_metadata: BTreeMap, - withheld_from_vlt: &BTreeSet, options: RewriteOptions<'_>, ) -> Rewritten { let CandidateFiles { @@ -2426,15 +2395,13 @@ async fn rewrite_once( // Pure CPU over every lock text (the independent rewriter groups // run concurrently inside), so it runs on the blocking pool rather // than on a runtime worker; `files` comes back for the probe below. - let withheld = withheld_from_vlt.clone(); tokio::task::spawn_blocking(move || { - let rewrite = rewrite_registry_redirect_withholding_vlt( + let rewrite = rewrite_registry_redirect_with_host( &files, &rewrite_overrides, &python_metadata, pipenv_major, bun_lockb, - &withheld, &gradle_unreadable, &yarn_outer, &cargo_outer, @@ -2447,13 +2414,12 @@ async fn rewrite_once( Err(e) => panic!("hosted rewrite task failed: {e}"), }) } else { - let rewrite = rewrite_registry_redirect_withholding_vlt( + let rewrite = rewrite_registry_redirect_with_host( &files, &rewrite_overrides, &python_metadata, pipenv_major, bun_lockb, - withheld_from_vlt, &gradle_unreadable, &yarn_outer, &cargo_outer, @@ -2590,7 +2556,7 @@ async fn rewrite_once( .filter(|k| !crate::patch::redirect::sbt::is_synthetic_key(k)) .cloned() .collect(); - let confirmed = confirm(&files, &rewrite, candidates, binary_bun, withheld_from_vlt); + let confirmed = confirm(&files, &rewrite, candidates, binary_bun); npm_warnings.extend(npm_deno_lock(view, &overrides, &confirmed)); Rewritten { files, @@ -3282,7 +3248,6 @@ fn confirm( rewrite: &RewriteResult, candidates: &[Candidate], binary_bun: bool, - withheld_from_vlt: &BTreeSet, ) -> Vec<(String, String)> { use crate::patch::redirect::pdm_drives; // A `pdm.lock` that is NOT the PyPI install driver (a `uv.lock` or @@ -3292,10 +3257,6 @@ fn confirm( // drive, pypi confirmation keys off `confirmed_pdm_uuids`, so dropping // the file is always safe. let pdm_inactive = files.contains_key("pdm.lock") && !pdm_drives(files); - // Likewise a `vlt-lock.json` the vlt rewrite was withheld from (its - // artifact failed the preflight beside another npm-family lock) may - // still hold an earlier run's pin: only the sibling lock this run - // rewrote can confirm that dep. // The yarn berry pin's `package.json` `resolutions` entry is only half of // it — the URL-keyed `yarn.lock` entry is what installs — so a hosted URL // left in the manifest (an earlier run, a refused rewrite) proves @@ -3471,37 +3432,17 @@ fn confirm( .any(|(_, text)| needles.iter().any(|n| text.contains(n.as_str()))), ); } - let needles = candidate_presence_needles(&c.dep); - if withheld_from_vlt.contains(uuid) { - ProbeStep::NeedlesOutsideVlt(needles) - } else { - ProbeStep::Needles(needles) - } + ProbeStep::Needles(candidate_presence_needles(&c.dep)) }) .collect(); - let groups = |outside_vlt: bool| -> Vec<&[String]> { - steps - .iter() - .filter_map(|step| match step { - ProbeStep::Needles(needles) if !outside_vlt => Some(needles.as_slice()), - ProbeStep::NeedlesOutsideVlt(needles) if outside_vlt => Some(needles.as_slice()), - _ => None, - }) - .collect() - }; - let mut present = groups_present(&needle_texts, &groups(false)).into_iter(); - let outside_vlt_groups = groups(true); - let mut present_outside_vlt = if outside_vlt_groups.is_empty() { - Vec::new() - } else { - let texts: Vec<&String> = final_texts - .iter() - .filter(|(name, _)| *name != VLT_LOCK && !is_gradle_file(name)) - .map(|(_, text)| *text) - .collect(); - groups_present(&texts, &outside_vlt_groups) - } - .into_iter(); + let groups: Vec<&[String]> = steps + .iter() + .filter_map(|step| match step { + ProbeStep::Needles(needles) => Some(needles.as_slice()), + ProbeStep::Decided(_) => None, + }) + .collect(); + let mut present = groups_present(&needle_texts, &groups).into_iter(); candidates .iter() .zip(&steps) @@ -3510,9 +3451,6 @@ fn confirm( ProbeStep::Needles(_) => present .next() .expect("one presence answer per needle group"), - ProbeStep::NeedlesOutsideVlt(_) => present_outside_vlt - .next() - .expect("one presence answer per needle group"), }) .map(|(c, _)| (c.purl.clone(), c.dep.patch_uuid.clone())) .collect() @@ -3999,15 +3937,7 @@ mod tests { let unreadable = BTreeSet::from([".cargo/config".to_string()]); let read = read_candidate_files(&view, &unreadable, &candidates).await; assert_eq!(read.unreadable_reads, vec![".cargo/config"]); - let done = rewrite( - &view, - read, - &candidates, - BTreeMap::new(), - &BTreeSet::new(), - options, - ) - .await; + let done = rewrite(&view, read, &candidates, BTreeMap::new(), options).await; assert_eq!( guard(&view, &done, &candidates).unwrap().code, UNREADABLE_REFUSAL @@ -4077,15 +4007,7 @@ mod tests { for view in [ProjectView::Disk(tmp.path()), ProjectView::Memory(&memory)] { let read = read_candidate_files(&view, &BTreeSet::new(), &candidates).await; assert_eq!(read.undecodable_reads, vec!["requirements.txt"]); - let done = rewrite( - &view, - read, - &candidates, - BTreeMap::new(), - &BTreeSet::new(), - options(), - ) - .await; + let done = rewrite(&view, read, &candidates, BTreeMap::new(), options()).await; let refusal = guard(&view, &done, &candidates).expect("refused"); assert_eq!(refusal.code, UNREADABLE_REFUSAL); assert!( @@ -4098,15 +4020,7 @@ mod tests { let (cargo_selected, cargo_refs) = cargo_reference("u-2"); let cargo = build_candidates(&cargo_selected, &cargo_refs, &mut Vec::new()); let read = read_candidate_files(&view, &BTreeSet::new(), &cargo).await; - let done = rewrite( - &view, - read, - &cargo, - BTreeMap::new(), - &BTreeSet::new(), - options(), - ) - .await; + let done = rewrite(&view, read, &cargo, BTreeMap::new(), options()).await; assert!(guard(&view, &done, &cargo).is_none()); } } @@ -4195,7 +4109,6 @@ mod tests { &RewriteResult::default(), std::slice::from_ref(&candidate), false, - &BTreeSet::new(), ); assert!(none.is_empty(), "{none:?}"); // The lock pin itself still confirms. @@ -4211,7 +4124,6 @@ mod tests { &RewriteResult::default(), std::slice::from_ref(&candidate), false, - &BTreeSet::new(), ); assert_eq!(confirmed.len(), 1, "{confirmed:?}"); } @@ -4256,13 +4168,7 @@ mod tests { files.insert("package.json".to_string(), manifest.clone()); let run = |files: &BTreeMap| { let rewrite = rewrite_registry_redirect(files, std::slice::from_ref(&candidate.dep)); - let confirmed = confirm( - files, - &rewrite, - std::slice::from_ref(&candidate), - false, - &BTreeSet::new(), - ); + let confirmed = confirm(files, &rewrite, std::slice::from_ref(&candidate), false); (rewrite, confirmed) }; let (first, confirmed) = run(&files); @@ -4383,15 +4289,7 @@ mod tests { }; let candidates = vec![left_pad_candidate()]; let read = read_candidate_files(view, unreadable, &candidates).await; - let done = rewrite( - view, - read.clone(), - &candidates, - BTreeMap::new(), - &BTreeSet::new(), - options, - ) - .await; + let done = rewrite(view, read.clone(), &candidates, BTreeMap::new(), options).await; (read, done) } @@ -4870,15 +4768,7 @@ snapshots: }; let read = read_candidate_files(&view, &BTreeSet::new(), &candidates).await; assert!(read.files.contains_key("package.json")); - let done = rewrite( - &view, - read, - &candidates, - BTreeMap::new(), - &BTreeSet::new(), - options, - ) - .await; + let done = rewrite(&view, read, &candidates, BTreeMap::new(), options).await; let skipped = done .rewrite .warnings @@ -4974,15 +4864,7 @@ snapshots: prior_discovery: None, }; let read = read_candidate_files(&view, &BTreeSet::new(), &candidates).await; - let done = rewrite( - &view, - read, - &candidates, - BTreeMap::new(), - &BTreeSet::new(), - options, - ) - .await; + let done = rewrite(&view, read, &candidates, BTreeMap::new(), options).await; assert!( done.rewrite.binary_files.contains_key("bun.lockb"), "{:?}", @@ -5151,15 +5033,7 @@ snapshots: }; let candidates = vec![gradle_candidate()]; let read = read_candidate_files(view, &BTreeSet::new(), &candidates).await; - let done = rewrite( - view, - read.clone(), - &candidates, - BTreeMap::new(), - &BTreeSet::new(), - options, - ) - .await; + let done = rewrite(view, read.clone(), &candidates, BTreeMap::new(), options).await; (read, done) } @@ -5556,15 +5430,7 @@ snapshots: let view = ProjectView::Disk(root); let candidates = vec![left_pad_candidate()]; let read = read_candidate_files(&view, &BTreeSet::new(), &candidates).await; - rewrite( - &view, - read, - &candidates, - BTreeMap::new(), - &BTreeSet::new(), - options, - ) - .await + rewrite(&view, read, &candidates, BTreeMap::new(), options).await } /// Lockfile discovery of `root` as the management commands make it. @@ -5772,15 +5638,7 @@ snapshots: let candidates = vec![candidate]; let view = ProjectView::Memory(&p); let read = read_candidate_files(&view, &BTreeSet::new(), &candidates).await; - let done = rewrite( - &view, - read, - &candidates, - BTreeMap::new(), - &BTreeSet::new(), - options, - ) - .await; + let done = rewrite(&view, read, &candidates, BTreeMap::new(), options).await; assert_eq!(done.confirmed.len(), 1, "{:?}", done.rewrite.warnings); assert!(done.unattributed.is_empty(), "{:?}", done.unattributed); let lockless: Vec<&RewriteWarning> = done @@ -5826,15 +5684,7 @@ snapshots: }; let candidates = vec![gem_candidate()]; let read = read_candidate_files(view, &BTreeSet::new(), &candidates).await; - let done = rewrite( - view, - read.clone(), - &candidates, - BTreeMap::new(), - &BTreeSet::new(), - options, - ) - .await; + let done = rewrite(view, read.clone(), &candidates, BTreeMap::new(), options).await; (read, done) } @@ -6029,15 +5879,7 @@ snapshots: let candidates = vec![candidate]; let view = ProjectView::Memory(&p); let read = read_candidate_files(&view, &BTreeSet::new(), &candidates).await; - let done = rewrite( - &view, - read, - &candidates, - BTreeMap::new(), - &BTreeSet::new(), - options, - ) - .await; + let done = rewrite(&view, read, &candidates, BTreeMap::new(), options).await; let ctx = format!("{label} ({eol:?})"); assert!( done.unattributed.is_empty(), @@ -6433,15 +6275,7 @@ snapshots: }; let candidates = is_number_candidates(); let read = read_candidate_files(view, &BTreeSet::new(), &candidates).await; - let done = rewrite( - view, - read.clone(), - &candidates, - BTreeMap::new(), - &BTreeSet::new(), - options, - ) - .await; + let done = rewrite(view, read.clone(), &candidates, BTreeMap::new(), options).await; (read, done) } diff --git a/crates/socket-patch-core/src/hosted/memory/stages.rs b/crates/socket-patch-core/src/hosted/memory/stages.rs index 63811b36b..d22d8fed7 100644 --- a/crates/socket-patch-core/src/hosted/memory/stages.rs +++ b/crates/socket-patch-core/src/hosted/memory/stages.rs @@ -14,10 +14,8 @@ use crate::hosted::engine::{ self, Candidate, CandidateFiles, Refusal, RewriteOptions, SkippedPatch, }; use crate::hosted::npm_manifest::HostedClassicArtifact; -use crate::hosted::vlt::Preflight; use crate::patch::redirect::npmrc::OuterAllowRemote; use crate::patch::redirect::yarnrc::OuterYarnMirror; -use crate::patch::redirect::DepOverride; use crate::utils::purl::strip_purl_qualifiers; use crate::vendor::lock_inventory::{MemoryEntry, MemoryProject, ProjectView}; use crate::vendor::VendorState; @@ -162,10 +160,6 @@ pub(crate) struct Planned { /// `(artifact url, sha512)` of every npm tarball a yarn classic pin /// reads (its sha1, #558, and its package.json, #591). pub(crate) npm_classic: Vec<(String, String)>, - /// The vlt artifact preflight, judged offline (no network here, so - /// every in-scope dep is withheld instead of pinned: `--offline` - /// parity). - pub(crate) vlt_preflight: Preflight, } /// Everything up to the wheel-metadata fetch. `unreadable` are the paths @@ -186,20 +180,7 @@ pub(crate) async fn plan( if engine::bun_lockb_symlinked(&view, &candidates) { return Err(engine::bun_lockb_symlink_refusal()); } - let vlt_preflight = { - let deps: Vec<(&str, &DepOverride)> = candidates - .iter() - .filter(|c| c.dep.ecosystem == "npm") - .map(|c| (c.purl.as_str(), &c.dep)) - .collect(); - crate::hosted::vlt::offline(&view, &deps).await - }; - engine::withhold_everywhere( - &mut candidates, - &vlt_preflight.withheld_everywhere, - &mut skipped, - ); - let mut pre_warnings = vlt_preflight.warnings.clone(); + let mut pre_warnings = Vec::new(); refuse_takeovers(&project, &mut candidates, &mut skipped, &mut pre_warnings); let read = if candidates.is_empty() { @@ -232,7 +213,6 @@ pub(crate) async fn plan( wheels, npm_manifests, npm_classic, - vlt_preflight, }) } @@ -273,7 +253,6 @@ pub(crate) async fn rewrite( wheels, npm_manifests, npm_classic, - vlt_preflight, } = planned; let skipped_before = skipped.clone(); let mut python_metadata: BTreeMap = BTreeMap::new(); @@ -357,7 +336,6 @@ pub(crate) async fn rewrite( read, &candidates, python_metadata, - &vlt_preflight.withheld_from_vlt, RewriteOptions { dry_run: options.dry_run, targets_pipenv_lock, diff --git a/crates/socket-patch-core/src/hosted/mod.rs b/crates/socket-patch-core/src/hosted/mod.rs index ae362abb4..d908c64da 100644 --- a/crates/socket-patch-core/src/hosted/mod.rs +++ b/crates/socket-patch-core/src/hosted/mod.rs @@ -9,7 +9,7 @@ //! ancestor directory governs the project). //! - [`guidance`] — the pnpm `trustLockfile` / npm `allow-remote` //! auto-config planners and their warning texts. -//! - [`vlt`] — the vlt artifact preflight. +//! - [`vlt`] — the vlt install-state and `bun.lockb` presence checks. //! - [`sbt_reads`] — the sbt resolution evidence a Maven candidate of an //! sbt build is gated on. //! - [`memory`] — the engine over an in-memory repository (no filesystem, diff --git a/crates/socket-patch-core/src/hosted/vlt.rs b/crates/socket-patch-core/src/hosted/vlt.rs index 834d182d6..9d69350ea 100644 --- a/crates/socket-patch-core/src/hosted/vlt.rs +++ b/crates/socket-patch-core/src/hosted/vlt.rs @@ -1,24 +1,10 @@ -//! The vlt artifact preflight of the hosted flow, over a [`ProjectView`]: -//! before any takeover or rewrite, each in-scope artifact is judged the way -//! vlt fetches it. The disk flow probes the artifacts over the network (or -//! judges them offline under `--offline`); the in-memory engine has no -//! network and judges every one offline. +//! vlt presence checks of the hosted flow, over a [`ProjectView`]: vlt's +//! install state and a binary `bun.lockb` sibling decide whether vlt drives +//! the install. -use std::collections::{BTreeMap, BTreeSet}; - -use crate::constants::npm_family::{BUN_LOCKB, VLT_HIDDEN_LOCK_REL, VLT_LOCK, VLT_STORE_DIR}; -use crate::formats::governing_locks::{npm_locks_outside, NpmLockFamily}; -use crate::patch::redirect::vlt_preflight::{self, ArtifactProbe, OFFLINE_REASON}; -use crate::patch::redirect::{vlt, DepOverride}; +use crate::constants::npm_family::{BUN_LOCKB, VLT_HIDDEN_LOCK_REL, VLT_STORE_DIR}; use crate::vendor::lock_inventory::{MemoryEntry, ProjectView}; -/// The warning code (and skip reason) of a dep whose artifact vlt would -/// fail to verify. -pub const ARTIFACT_UNVERIFIABLE: &str = "redirect_vlt_artifact_unverifiable"; - -/// The skip `reason` of a dep the preflight withheld from every rewriter. -pub const WITHHELD_REASON: &str = ARTIFACT_UNVERIFIABLE; - /// Whether vlt's install state exists: the hidden lock as a regular file, /// or the store as a real directory. Stat only; the hidden lock can be /// megabytes and is never read into the rewriter's input. @@ -56,164 +42,3 @@ pub(crate) fn bun_lockb_present(view: &ProjectView<'_>) -> bool { ProjectView::Memory(project) => project.contains(BUN_LOCKB), } } - -/// What the artifact preflight decided for this run's npm candidates. -#[derive(Debug, Clone, Default)] -pub struct Preflight { - /// Failed while vlt drives, or for a vlt-vendored takeover: withheld - /// from every rewriter. - pub withheld_everywhere: BTreeMap, - /// Failed while another npm-family lock may drive: kept out of the vlt - /// rewrite only. - pub withheld_from_vlt: BTreeSet, - pub passed: BTreeSet, - /// Artifact bytes by URL, for the heal's no-record comparison. - pub artifacts: BTreeMap>, - pub warnings: Vec, -} - -/// The files `vlt_drives` and the preflight scope read: `vlt-lock.json` -/// itself, and presence-only entries for the sibling locks and vlt's -/// install state. Empty without a readable `vlt-lock.json`. -pub async fn inputs(view: &ProjectView<'_>) -> BTreeMap { - let mut files = BTreeMap::new(); - let Ok(lock) = view.read_text(VLT_LOCK).await else { - return files; - }; - files.insert(VLT_LOCK.to_string(), lock); - // `bun.lockb` stays out: `vlt_drives` takes it as `bun_lockb_present`. - for sibling in npm_locks_outside(NpmLockFamily::Vlt, |lock| { - lock != BUN_LOCKB && view.is_file(lock) - }) { - files.insert(sibling.to_string(), String::new()); - } - if install_state_present(view) { - files.insert(VLT_HIDDEN_LOCK_REL.to_string(), String::new()); - } - files -} - -/// The deps a preflight must judge, and whether vlt drives the install. -pub struct PreflightPlan { - pub scope: Vec, - pub drives: bool, -} - -impl PreflightPlan { - /// The distinct artifact URLs to probe. - pub fn urls(&self) -> BTreeSet { - self.scope.iter().map(|d| d.artifact_url.clone()).collect() - } -} - -/// The preflight's scope over the project's [`inputs`] (`files`, never -/// empty): `None` when no dep is in scope. `vendored` are the uuids whose -/// purl a vlt vendored ledger entry claims. -pub fn plan( - view: &ProjectView<'_>, - files: &BTreeMap, - deps: &[(&str, &DepOverride)], - vendored: &BTreeSet, -) -> Option { - let overrides: Vec = deps.iter().map(|(_, dep)| (*dep).clone()).collect(); - let scope = vlt_preflight::preflight_scope(files, &overrides, vendored); - if scope.is_empty() { - return None; - } - let drives = vlt::vlt_drives(files, bun_lockb_present(view)); - Some(PreflightPlan { scope, drives }) -} - -fn unverifiable_detail( - url: &str, - reason: &str, - purl: &str, - already_pinned: bool, - everywhere: bool, -) -> String { - if already_pinned { - format!( - "vlt would fail to verify {url}: {reason}; {purl} was left pinned by an earlier run \ - and `vlt ci` will fail until the artifact verifies" - ) - } else if everywhere { - format!("vlt would fail to verify {url}: {reason}; nothing was written for {purl}") - } else { - format!( - "vlt would fail to verify {url}: {reason}; vlt-lock.json was not changed for {purl}" - ) - } -} - -/// The preflight's verdicts for `plan` given the `probes` fetched for it -/// (none for a URL that was not fetched: judged offline). -pub fn judge( - plan: &PreflightPlan, - deps: &[(&str, &DepOverride)], - probes: BTreeMap, -) -> Preflight { - let mut out = Preflight::default(); - let mut passed_urls: BTreeSet<&str> = BTreeSet::new(); - for dep in &plan.scope { - let reason = match probes.get(&dep.artifact_url) { - None => Some(OFFLINE_REASON.to_string()), - Some(probe) => probe.failure(&dep.sha512), - }; - let Some(reason) = reason else { - out.passed.insert(dep.patch_uuid.clone()); - passed_urls.insert(&dep.artifact_url); - continue; - }; - let purl = deps - .iter() - .find(|(_, d)| d.patch_uuid == dep.patch_uuid) - .map_or("", |(purl, _)| *purl); - let everywhere = plan.drives || dep.vendored; - let detail = unverifiable_detail( - &dep.artifact_url, - &reason, - purl, - dep.already_pinned, - everywhere, - ); - out.warnings.push(crate::hosted::engine::warning( - ARTIFACT_UNVERIFIABLE, - crate::hosted::engine::redact_artifact_text( - &detail, - &dep.artifact_url, - &dep.patch_uuid, - ), - )); - if everywhere { - out.withheld_everywhere - .insert(dep.patch_uuid.clone(), purl.to_string()); - } else { - out.withheld_from_vlt.insert(dep.patch_uuid.clone()); - } - } - // Moved, not copied: every dep has been judged, and the probes are not - // read again, so a verified body is held once. - for (url, probe) in probes { - if passed_urls.contains(url.as_str()) { - if let Some(body) = probe.body { - out.artifacts.insert(url, body); - } - } - } - out -} - -/// The preflight for a host with no network: every in-scope artifact is -/// judged as `--offline` judges it, so the dep is withheld -/// (`redirect_vlt_artifact_unverifiable`, "offline") rather than pinned in -/// a lock vlt may not be able to install. -pub async fn offline(view: &ProjectView<'_>, deps: &[(&str, &DepOverride)]) -> Preflight { - let files = inputs(view).await; - if files.is_empty() { - return Preflight::default(); - } - match plan(view, &files, deps, &BTreeSet::new()) { - Some(plan) => judge(&plan, deps, BTreeMap::new()), - None => Preflight::default(), - } -} diff --git a/crates/socket-patch-core/src/patch/redirect/group_equivalence_tests.rs b/crates/socket-patch-core/src/patch/redirect/group_equivalence_tests.rs index 7bbf2bed5..e012bc418 100644 --- a/crates/socket-patch-core/src/patch/redirect/group_equivalence_tests.rs +++ b/crates/socket-patch-core/src/patch/redirect/group_equivalence_tests.rs @@ -133,7 +133,6 @@ fn assert_same_with_metadata( let groups = rewriter_groups( files, &overrides, - &overrides, bun_lockb_present, python_metadata, &no_gradle_unreadable, diff --git a/crates/socket-patch-core/src/patch/redirect/mod.rs b/crates/socket-patch-core/src/patch/redirect/mod.rs index 0c778ac74..bdc59755e 100644 --- a/crates/socket-patch-core/src/patch/redirect/mod.rs +++ b/crates/socket-patch-core/src/patch/redirect/mod.rs @@ -109,7 +109,6 @@ mod state; pub mod upstream; pub mod vlt; pub mod vlt_heal; -pub mod vlt_preflight; pub mod yarnrc; /// Hosted-artifact leaf ownership rule, shared with `vex`'s bun lockfile /// discovery (which recovers a URL tuple's version from that leaf). @@ -597,14 +596,13 @@ pub fn rewrite_registry_redirect_with_pipenv_version( pipenv_major: Option, bun_lockb_present: bool, ) -> RewriteResult { - rewrite_registry_redirect_withholding_vlt( + rewrite_registry_redirect_with_host( files, overrides, artifact_metadata, pipenv_major, bun_lockb_present, &std::collections::BTreeSet::new(), - &std::collections::BTreeSet::new(), &yarnrc::OuterYarnMirror::default(), &CratesIoSource::Registry, ) @@ -670,20 +668,17 @@ fn withhold_pypi_platform_wheels<'a>( withhold(overrides, &refused) } -/// [`rewrite_registry_redirect_with_pipenv_version`] with the patch uuids -/// in `vlt_withheld` kept out of the vlt rewrite only: their artifact -/// failed vlt's preflight while another npm-family lock may be the one the -/// project installs from. `gradle_unreadable` are the Gradle build files +/// [`rewrite_registry_redirect_with_pipenv_version`] with the inputs only +/// the host can resolve. `gradle_unreadable` are the Gradle build files /// the host found but could not read as text: the Gradle planner refuses /// the build rather than take them for absent. #[allow(clippy::too_many_arguments)] -pub fn rewrite_registry_redirect_withholding_vlt( +pub fn rewrite_registry_redirect_with_host( files: &BTreeMap, overrides: &[DepOverride], artifact_metadata: &BTreeMap, pipenv_major: Option, bun_lockb_present: bool, - vlt_withheld: &std::collections::BTreeSet, gradle_unreadable: &std::collections::BTreeSet, yarn_outer: &yarnrc::OuterYarnMirror, cargo_outer: &CratesIoSource, @@ -704,11 +699,9 @@ pub fn rewrite_registry_redirect_withholding_vlt( // rewriters too (see `pipenv::rewrite`). pipenv::rewrite(files, &overrides, pipenv_major, &mut result); let overrides = withhold(&overrides, &result.refused_pipenv_uuids); - let vlt_overrides = withhold(&overrides, vlt_withheld); let groups = rewriter_groups( files, &overrides, - &vlt_overrides, bun_lockb_present, artifact_metadata, gradle_unreadable, @@ -730,13 +723,11 @@ type RewriterGroup<'a> = Box; /// rewriter's confirmations, `rewrite_uv_lock` the python lock sets and a /// metadata file an earlier python rewriter may have written) or the /// withholding prefix's. Every group sees the full withheld override slice, -/// exactly as the serial chain did; the vlt rewriter sees `vlt_overrides`, -/// that slice minus the vlt-withheld uuids. +/// exactly as the serial chain did. #[allow(clippy::too_many_arguments)] fn rewriter_groups<'a>( files: &'a BTreeMap, overrides: &'a [DepOverride], - vlt_overrides: &'a [DepOverride], bun_lockb_present: bool, artifact_metadata: &'a BTreeMap, gradle_unreadable: &'a std::collections::BTreeSet, @@ -751,9 +742,7 @@ fn rewriter_groups<'a>( rewrite_yarn_berry_with_manifests(files, overrides, artifact_metadata, result); rewrite_bun_lock(files, overrides, result); }), - Box::new(move |result| { - vlt::rewrite_vlt_lock(files, vlt_overrides, bun_lockb_present, result) - }), + Box::new(move |result| vlt::rewrite_vlt_lock(files, overrides, bun_lockb_present, result)), Box::new(move |result| { requirements::rewrite(files, overrides, result); rewrite_hatch(files, overrides, result); @@ -13613,14 +13602,13 @@ mod tests { ) .is_err()); // The full chain drives it too. - let mut r = rewrite_registry_redirect_withholding_vlt( + let mut r = rewrite_registry_redirect_with_host( &BTreeMap::from([("yarn.lock".to_string(), classic_lock_two_entries())]), std::slice::from_ref(&ovr), &BTreeMap::new(), None, false, &std::collections::BTreeSet::new(), - &std::collections::BTreeSet::new(), &refusing[3], &CratesIoSource::Registry, ); diff --git a/crates/socket-patch-core/src/patch/redirect/vlt.rs b/crates/socket-patch-core/src/patch/redirect/vlt.rs index f2c26c4dd..e4bc59b82 100644 --- a/crates/socket-patch-core/src/patch/redirect/vlt.rs +++ b/crates/socket-patch-core/src/patch/redirect/vlt.rs @@ -34,7 +34,7 @@ fn sibling_locks(files: &BTreeMap, bun_lockb_present: bool) -> V }) } -/// Does vlt drive hosted confirmation and the artifact preflight? +/// Does vlt drive hosted confirmation? /// `vlt-lock.json` must be present, and either vlt's install state (the /// `node_modules/.vlt-lock.json` sentinel) is too, or no other npm-family /// lock is (`bun_lockb_present`: see [`sibling_locks`]). Otherwise both @@ -57,7 +57,7 @@ fn lock_unsupported(detail: &str) -> RewriteWarning { /// A lock that passed the lock-level parse, with its nodes section located /// and its node ids split once. -pub(super) struct HostedLock { +struct HostedLock { parsed: ParsedLock, nodes: Option, index: NodeIndex, @@ -127,7 +127,7 @@ impl NodeIndex { } } -pub(super) fn parse_hosted_lock(text: &str) -> Result { +fn parse_hosted_lock(text: &str) -> Result { let parsed = match sniff_lock(text) { LockSniff::Readable(parsed) => parsed, LockSniff::Bom => { @@ -192,18 +192,6 @@ fn outside_instances<'a>(lock: &'a HostedLock, name: &str, version: &str) -> Vec .collect() } -/// The default-registry node ids of `dep` in a lock that passed the -/// lock-level parse. -pub(super) fn default_instances<'a>(lock: &'a HostedLock, dep: &DepOverride) -> Vec<&'a str> { - partition_instances(lock, &full_name(dep), &dep.version).0 -} - -/// Does `dep` have a node of socket-patch's vendored vlt shape in a -/// lock that passed the lock-level parse? -pub(super) fn has_vendored_instance(lock: &HostedLock, dep: &DepOverride) -> bool { - has_vendored_node(lock, &full_name(dep), &dep.version) -} - /// The lock-level warnings that say vlt may discard the lock's pins: /// neither in-run nor standalone VEX attests a hosted pin of such a lock /// from the lock alone. @@ -484,14 +472,6 @@ fn instances_pinned(nodes: &Map, ids: &[&str], sha512: &str, url: }) } -/// Did an earlier run pin every one of `ids`? Reads the lock as parsed, -/// not its text again. -pub(super) fn already_pinned(lock: &HostedLock, ids: &[&str], sha512: &str, url: &str) -> bool { - lock.parsed - .nodes() - .is_some_and(|nodes| instances_pinned(nodes, ids, sha512, url)) -} - struct Splice { line: usize, text: String, diff --git a/crates/socket-patch-core/src/patch/redirect/vlt_heal.rs b/crates/socket-patch-core/src/patch/redirect/vlt_heal.rs index 13a28b124..cfa4fe362 100644 --- a/crates/socket-patch-core/src/patch/redirect/vlt_heal.rs +++ b/crates/socket-patch-core/src/patch/redirect/vlt_heal.rs @@ -9,7 +9,7 @@ //! reinstall (an optional node), and nothing outside the project root is //! ever touched. -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::BTreeSet; use std::path::{Path, PathBuf}; use serde_json::{Map, Value}; @@ -19,7 +19,6 @@ use crate::constants::npm_family::{VLT_HIDDEN_LOCK_REL, VLT_STORE_DIR}; use crate::manifest::schema::PatchRecord; use crate::patch::apply::{is_safe_relative_subpath, normalize_file_path}; use crate::patch::file_hash::compute_file_git_sha256; -use crate::patch::package::read_archive_bytes_to_map_strict; use crate::utils::purl_key::PurlKey; use crate::vendor::vlt_lock_text::{ is_default_registry, is_registry_package_name, sniff_lock, split_dep_id, DepIdKind, LockSniff, @@ -94,8 +93,6 @@ pub struct Target<'a> { /// node is gone or the slot is null). pub lock_sha512: Option<&'a str>, pub record: Option<&'a PatchRecord>, - /// The artifact bytes the preflight downloaded. - pub artifact: Option<&'a [u8]>, /// Slot [0] of the node, see [`reinstalls_after_removal`]. pub flags: Option, } @@ -314,62 +311,10 @@ async fn record_check(dir: &Path, record: &PatchRecord, expected: Expected) -> B } } -/// The regular files under `dir` (its own `node_modules/` excluded) by -/// `/`-joined relative path; `Err(true)` when a link or special file is -/// present (never an extracted artifact), `Err(false)` when unreadable. -fn installed_files(dir: &Path) -> Result>, bool> { - let mut out = BTreeMap::new(); - let walk = walkdir::WalkDir::new(dir) - .follow_links(false) - .into_iter() - .filter_entry(|e| !(e.depth() == 1 && e.file_name() == "node_modules")); - for entry in walk { - let entry = entry.map_err(|_| false)?; - if entry.depth() == 0 || entry.file_type().is_dir() { - continue; - } - if !entry.file_type().is_file() { - return Err(true); - } - let rel = entry.path().strip_prefix(dir).map_err(|_| false)?; - let key = rel - .components() - .map(|c| c.as_os_str().to_str()) - .collect::>>() - .ok_or(false)? - .join("/"); - out.insert(key, std::fs::read(entry.path()).map_err(|_| false)?); - } - Ok(out) -} - -fn artifact_check(dir: &Path, artifact: &[u8]) -> ByteCheck { - let Ok(expected) = read_archive_bytes_to_map_strict(artifact) else { - return ByteCheck::Unknown; - }; - let installed = match installed_files(dir) { - Ok(files) => files, - Err(true) => return ByteCheck::Mismatch, - Err(false) => return ByteCheck::Unknown, - }; - let expected: BTreeMap> = expected - .into_iter() - .filter(|(path, _)| path != "node_modules" && !path.starts_with("node_modules/")) - .collect(); - if expected == installed { - ByteCheck::Match - } else { - ByteCheck::Mismatch - } -} - async fn bytes_check(dir: &Path, target: &Target<'_>, expected: Expected) -> ByteCheck { - if let Some(record) = target.record.filter(|r| !r.files.is_empty()) { - return record_check(dir, record, expected).await; - } - match (expected, target.artifact) { - (Expected::Patched, Some(artifact)) => artifact_check(dir, artifact), - _ => ByteCheck::Unknown, + match target.record.filter(|r| !r.files.is_empty()) { + Some(record) => record_check(dir, record, expected).await, + None => ByteCheck::Unknown, } } @@ -535,13 +480,12 @@ mod tests { .unwrap(); } - fn target<'a>(record: Option<&'a PatchRecord>, artifact: Option<&'a [u8]>) -> Target<'a> { + fn target(record: Option<&PatchRecord>) -> Target<'_> { Target { dep_id: ID, name: "left-pad", lock_sha512: Some(LOCK_SHA), record, - artifact, flags: Some(0), } } @@ -551,19 +495,6 @@ mod tests { classify_target(&state, root, target, expected).await } - fn tarball(entries: &[(&str, &[u8])]) -> Vec { - let gz = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default()); - let mut builder = tar::Builder::new(gz); - for (name, data) in entries { - let mut header = tar::Header::new_gnu(); - header.set_size(data.len() as u64); - header.set_mode(0o644); - header.set_cksum(); - builder.append_data(&mut header, name, *data).unwrap(); - } - builder.into_inner().unwrap().finish().unwrap() - } - #[tokio::test] async fn rule_a_hidden_lock_integrity_differs() { let tmp = tempfile::tempdir().unwrap(); @@ -571,7 +502,7 @@ mod tests { hidden(tmp.path(), Some("sha512-UPSTREAM")); let rec = record(); assert_eq!( - classify(tmp.path(), &target(Some(&rec), None), Expected::Patched).await, + classify(tmp.path(), &target(Some(&rec)), Expected::Patched).await, TargetState::Stale ); } @@ -583,7 +514,7 @@ mod tests { hidden(tmp.path(), None); let rec = record(); assert_eq!( - classify(tmp.path(), &target(Some(&rec), None), Expected::Patched).await, + classify(tmp.path(), &target(Some(&rec)), Expected::Patched).await, TargetState::Stale ); } @@ -595,12 +526,12 @@ mod tests { hidden(tmp.path(), Some(LOCK_SHA)); let rec = record(); assert_eq!( - classify(tmp.path(), &target(Some(&rec), None), Expected::Patched).await, + classify(tmp.path(), &target(Some(&rec)), Expected::Patched).await, TargetState::Stale ); std::fs::remove_file(tmp.path().join(VLT_HIDDEN_LOCK_REL)).unwrap(); assert_eq!( - classify(tmp.path(), &target(Some(&rec), None), Expected::Patched).await, + classify(tmp.path(), &target(Some(&rec)), Expected::Patched).await, TargetState::Stale ); } @@ -612,122 +543,59 @@ mod tests { hidden(tmp.path(), Some(LOCK_SHA)); let rec = record(); assert_eq!( - classify(tmp.path(), &target(Some(&rec), None), Expected::Patched).await, + classify(tmp.path(), &target(Some(&rec)), Expected::Patched).await, TargetState::Healthy ); std::fs::remove_file(tmp.path().join(VLT_HIDDEN_LOCK_REL)).unwrap(); assert_eq!( - classify(tmp.path(), &target(Some(&rec), None), Expected::Patched).await, + classify(tmp.path(), &target(Some(&rec)), Expected::Patched).await, TargetState::Healthy, "a tree without a hidden lock is judged by its bytes" ); assert_eq!( - classify(tmp.path(), &target(Some(&rec), None), Expected::Pristine).await, + classify(tmp.path(), &target(Some(&rec)), Expected::Pristine).await, TargetState::Stale, "patched bytes are stale once the pin is restored" ); store(tmp.path(), ID, PRISTINE); assert_eq!( - classify(tmp.path(), &target(Some(&rec), None), Expected::Pristine).await, + classify(tmp.path(), &target(Some(&rec)), Expected::Pristine).await, TargetState::Healthy ); } #[tokio::test] - async fn undeterminable_without_hidden_lock_record_or_artifact() { + async fn undeterminable_without_hidden_lock_or_record() { let tmp = tempfile::tempdir().unwrap(); store(tmp.path(), ID, PATCHED); assert_eq!( - classify(tmp.path(), &target(None, None), Expected::Patched).await, + classify(tmp.path(), &target(None), Expected::Patched).await, TargetState::Undeterminable ); hidden(tmp.path(), Some(LOCK_SHA)); assert_eq!( - classify(tmp.path(), &target(None, None), Expected::Patched).await, + classify(tmp.path(), &target(None), Expected::Patched).await, TargetState::Healthy, "a parsed hidden lock that agrees decides alone" ); std::fs::write(tmp.path().join(VLT_HIDDEN_LOCK_REL), b"{not json").unwrap(); assert_eq!( - classify(tmp.path(), &target(None, None), Expected::Pristine).await, + classify(tmp.path(), &target(None), Expected::Pristine).await, TargetState::Undeterminable ); } - #[tokio::test] - async fn no_record_compares_against_the_artifact_bytes() { - let tmp = tempfile::tempdir().unwrap(); - store(tmp.path(), ID, PATCHED); - let dir = package_dir(tmp.path(), ID, "left-pad"); - std::fs::create_dir_all(dir.join("node_modules/dep")).unwrap(); - std::fs::write(dir.join("node_modules/dep/x.js"), b"ignored").unwrap(); - let good = tarball(&[ - ("package/index.js", PATCHED), - ("package/package.json", b"{\"name\":\"left-pad\"}"), - ]); - let bad = tarball(&[ - ("package/index.js", PRISTINE), - ("package/package.json", b"{\"name\":\"left-pad\"}"), - ]); - assert_eq!( - classify(tmp.path(), &target(None, Some(&good)), Expected::Patched).await, - TargetState::Healthy - ); - assert_eq!( - classify(tmp.path(), &target(None, Some(&bad)), Expected::Patched).await, - TargetState::Stale - ); - std::fs::write(dir.join("extra.js"), b"x").unwrap(); - assert_eq!( - classify(tmp.path(), &target(None, Some(&good)), Expected::Patched).await, - TargetState::Stale, - "an extra installed file is not the artifact" - ); - assert_eq!( - classify( - tmp.path(), - &target(None, Some(b"not a tarball")), - Expected::Patched - ) - .await, - TargetState::Undeterminable - ); - } - - #[tokio::test] - async fn bundled_node_modules_in_the_artifact_are_not_compared() { - let tmp = tempfile::tempdir().unwrap(); - store(tmp.path(), ID, PATCHED); - let dir = package_dir(tmp.path(), ID, "left-pad"); - std::fs::create_dir_all(dir.join("node_modules/x")).unwrap(); - std::fs::write(dir.join("node_modules/x/index.js"), b"bundled").unwrap(); - let bundling = tarball(&[ - ("package/index.js", PATCHED), - ("package/package.json", b"{\"name\":\"left-pad\"}"), - ("package/node_modules/x/index.js", b"bundled"), - ]); - assert_eq!( - classify( - tmp.path(), - &target(None, Some(&bundling)), - Expected::Patched - ) - .await, - TargetState::Healthy - ); - } - #[tokio::test] async fn an_uninstalled_target_is_healthy() { let tmp = tempfile::tempdir().unwrap(); let rec = record(); assert_eq!( - classify(tmp.path(), &target(Some(&rec), None), Expected::Patched).await, + classify(tmp.path(), &target(Some(&rec)), Expected::Patched).await, TargetState::Healthy ); std::fs::create_dir_all(tmp.path().join(VLT_STORE_DIR)).unwrap(); assert_eq!( - classify(tmp.path(), &target(Some(&rec), None), Expected::Patched).await, + classify(tmp.path(), &target(Some(&rec)), Expected::Patched).await, TargetState::Healthy ); } @@ -754,13 +622,7 @@ mod tests { let rec = record(); let state = read_install_state(&project).await; assert_eq!( - classify_target( - &state, - &project, - &target(Some(&rec), None), - Expected::Patched - ) - .await, + classify_target(&state, &project, &target(Some(&rec)), Expected::Patched).await, TargetState::Undeterminable, "{linked}" ); diff --git a/crates/socket-patch-core/src/patch/redirect/vlt_preflight.rs b/crates/socket-patch-core/src/patch/redirect/vlt_preflight.rs deleted file mode 100644 index 6482882e7..000000000 --- a/crates/socket-patch-core/src/patch/redirect/vlt_preflight.rs +++ /dev/null @@ -1,527 +0,0 @@ -//! The hosted vlt artifact preflight (`redirect_vlt_artifact_unverifiable`). -//! -//! vlt always sends `accept-encoding: gzip`, and it hashes the bytes it -//! received, so a server that re-gzips the archive makes every install fail -//! `EINTEGRITY`. Before a vlt lock is pinned to a hosted artifact, the -//! artifact is fetched the way vlt fetches it and hashed raw: the workspace -//! reqwest is built without gzip/brotli, so bodies are never decoded. - -use std::collections::{BTreeMap, BTreeSet}; -use std::time::Duration; - -use crate::utils::digest::sha512_sri_of; - -use super::{vlt, DepOverride}; -use crate::api::client::{ApiClient, MAX_VENDOR_PACKAGE_BYTES}; -use crate::constants::npm_family::VLT_LOCK; -use crate::utils::http::{read_capped_typed, ReadCappedError}; - -/// The `accept-encoding` vlt sends for registry tarballs. -pub const VLT_ACCEPT_ENCODING: &str = "gzip;q=1.0, identity;q=0.5"; - -/// The failure reason for a run that may not touch the network. -pub const OFFLINE_REASON: &str = "offline"; - -const MAX_CONCURRENT_PROBES: usize = 4; -const HEADERS_TIMEOUT: Duration = Duration::from_secs(60); -const BODY_TIMEOUT: Duration = Duration::from_secs(300); - -/// What fetching one artifact URL as vlt does returned. -#[derive(Debug, Clone, Default, PartialEq, Eq)] -pub struct ArtifactProbe { - pub status: Option, - pub content_encoding: Option, - /// SRI form (`sha512-`) of the raw body. - pub sha512: Option, - /// The raw body, kept for the warm-tree heal's no-record comparison. - pub body: Option>, - pub error: Option, -} - -impl ArtifactProbe { - /// Why vlt would fail to verify this artifact against `sha512`, or - /// `None` when it passes. - pub fn failure(&self, sha512: &str) -> Option { - if let Some(error) = &self.error { - return Some(format!("fetch error {error}")); - } - match self.status { - Some(200) => {} - Some(status) => return Some(format!("http {status}")), - None => return Some("fetch error no response".to_string()), - } - if let Some(encoding) = self - .content_encoding - .as_deref() - .map(str::trim) - .filter(|e| !e.is_empty() && !e.eq_ignore_ascii_case("identity")) - { - return Some(format!("content-encoding {encoding}")); - } - if self.sha512.as_deref() != Some(sha512) { - return Some("sha512 mismatch".to_string()); - } - None - } -} - -fn fetch_error(error: impl Into) -> ArtifactProbe { - ArtifactProbe { - error: Some(error.into()), - ..ArtifactProbe::default() - } -} - -/// `GET url` with vlt's `accept-encoding` (and reqwest's default -/// `Accept: */*`), following up to ten redirects, the body capped like -/// every other artifact download. -pub async fn fetch_artifact_probe(client: &reqwest::Client, url: &str) -> ArtifactProbe { - fetch_capped(client, url, MAX_VENDOR_PACKAGE_BYTES).await -} - -async fn fetch_capped(client: &reqwest::Client, url: &str, max: u64) -> ArtifactProbe { - if !(url.starts_with("https://") || url.starts_with("http://")) { - return fetch_error("refusing a non-http(s) artifact URL"); - } - let sent = tokio::time::timeout( - HEADERS_TIMEOUT, - client - .get(url) - .header(reqwest::header::ACCEPT_ENCODING, VLT_ACCEPT_ENCODING) - .send(), - ) - .await; - let resp = match sent { - Ok(Ok(resp)) => resp, - Ok(Err(e)) => return fetch_error(e.without_url().to_string()), - Err(_) => return fetch_error(format!("no response within {HEADERS_TIMEOUT:?}")), - }; - let status = resp.status().as_u16(); - let content_encoding = resp - .headers() - .get(reqwest::header::CONTENT_ENCODING) - .map(|v| String::from_utf8_lossy(v.as_bytes()).into_owned()); - if status != 200 { - return ArtifactProbe { - status: Some(status), - content_encoding, - ..ArtifactProbe::default() - }; - } - let body = tokio::time::timeout( - BODY_TIMEOUT, - read_capped_typed(resp, max, "hosted artifact"), - ) - .await - .unwrap_or_else(|_| { - Err(ReadCappedError::Truncated(format!( - "hosted artifact body not received within {BODY_TIMEOUT:?}" - ))) - }); - match body { - Ok(bytes) => ArtifactProbe { - status: Some(status), - content_encoding, - sha512: Some(sha512_sri_of(&bytes)), - body: Some(bytes), - error: None, - }, - Err(e) => ArtifactProbe { - status: Some(status), - content_encoding, - error: Some(e.to_string()), - ..ArtifactProbe::default() - }, - } -} - -/// Probe every distinct URL in `urls` through `api`'s plain client (no -/// `Authorization`: the grant token is in the URL), at most four at a time. -pub async fn probe_artifacts( - api: &ApiClient, - urls: &BTreeSet, -) -> BTreeMap { - let semaphore = std::sync::Arc::new(tokio::sync::Semaphore::new(MAX_CONCURRENT_PROBES)); - let mut tasks = tokio::task::JoinSet::new(); - for url in urls { - let client = api.plain_http().clone(); - let semaphore = semaphore.clone(); - let url = url.clone(); - tasks.spawn(async move { - let _permit = semaphore.acquire_owned().await; - let probe = fetch_artifact_probe(&client, &url).await; - (url, probe) - }); - } - let mut out = BTreeMap::new(); - while let Some(joined) = tasks.join_next().await { - if let Ok((url, probe)) = joined { - out.insert(url, probe); - } - } - for url in urls { - out.entry(url.clone()) - .or_insert_with(|| fetch_error("probe task failed")); - } - out -} - -/// One override the preflight must probe. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PreflightDep { - pub patch_uuid: String, - pub artifact_url: String, - pub sha512: String, - /// Every default-registry instance already carries this URL and sha512 - /// (an earlier run pinned it). - pub already_pinned: bool, - /// In scope only through its vendored node: a vendored-to-hosted - /// takeover restores the registry node this run would pin. - pub vendored: bool, -} - -/// The npm overrides the preflight probes: `vlt-lock.json` passes the -/// lock-level parse, the override has a sha512, and it has at least one -/// default-registry instance in the lock, or (for the uuids in -/// `vendored_vlt`, whose purl a vlt vendored entry claims) a vendored node -/// the hosted takeover will turn back into one. Nothing is probed without -/// a `vlt-lock.json`. -pub fn preflight_scope( - files: &BTreeMap, - overrides: &[DepOverride], - vendored_vlt: &BTreeSet, -) -> Vec { - let Some(text) = files.get(VLT_LOCK) else { - return Vec::new(); - }; - let Ok(lock) = vlt::parse_hosted_lock(text) else { - return Vec::new(); - }; - overrides - .iter() - .filter(|dep| dep.ecosystem == "npm") - .filter_map(|dep| { - let sha512 = dep.integrity.sha512.as_deref().filter(|s| !s.is_empty())?; - let ids = vlt::default_instances(&lock, dep); - let vendored = ids.is_empty() - && vendored_vlt.contains(&dep.patch_uuid) - && vlt::has_vendored_instance(&lock, dep); - if ids.is_empty() && !vendored { - return None; - } - let already_pinned = - !vendored && vlt::already_pinned(&lock, &ids, sha512, &dep.artifact_url); - Some(PreflightDep { - patch_uuid: dep.patch_uuid.clone(), - artifact_url: dep.artifact_url.clone(), - sha512: sha512.to_string(), - already_pinned, - vendored, - }) - }) - .collect() -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::api::client::ApiClientOptions; - use crate::patch::redirect::Integrity; - use std::io::Write as _; - use wiremock::matchers::{method, path}; - use wiremock::{Mock, MockServer, Request, ResponseTemplate}; - - const BODY: &[u8] = b"patched tarball bytes"; - - fn api(token: Option<&str>) -> ApiClient { - ApiClient::new(ApiClientOptions { - api_url: "http://127.0.0.1:9".to_string(), - api_token: token.map(str::to_string), - route: crate::api::client::ApiRoute::org("org"), - }) - } - - async fn probe_of(server: &MockServer, route: &str) -> ArtifactProbe { - fetch_artifact_probe(api(None).plain_http(), &format!("{}{route}", server.uri())).await - } - - fn gzip(bytes: &[u8]) -> Vec { - let mut enc = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default()); - enc.write_all(bytes).unwrap(); - enc.finish().unwrap() - } - - #[tokio::test] - async fn identity_body_with_the_granted_sha512_passes() { - let server = MockServer::start().await; - Mock::given(method("GET")) - .and(path("/a.tgz")) - .respond_with(ResponseTemplate::new(200).set_body_bytes(BODY)) - .mount(&server) - .await; - let probe = probe_of(&server, "/a.tgz").await; - assert_eq!(probe.failure(&sha512_sri_of(BODY)), None); - assert_eq!(probe.body.as_deref(), Some(BODY)); - } - - #[tokio::test] - async fn an_explicit_identity_encoding_passes() { - let server = MockServer::start().await; - Mock::given(method("GET")) - .respond_with( - ResponseTemplate::new(200) - .insert_header("content-encoding", "identity") - .set_body_bytes(BODY), - ) - .mount(&server) - .await; - assert_eq!( - probe_of(&server, "/a.tgz") - .await - .failure(&sha512_sri_of(BODY)), - None - ); - } - - #[tokio::test] - async fn every_failure_reason() { - let server = MockServer::start().await; - let gz = gzip(BODY); - Mock::given(method("GET")) - .and(path("/gz.tgz")) - .respond_with( - ResponseTemplate::new(200) - .insert_header("content-encoding", "gzip") - .set_body_bytes(gz.clone()), - ) - .mount(&server) - .await; - Mock::given(method("GET")) - .and(path("/other.tgz")) - .respond_with(ResponseTemplate::new(200).set_body_bytes(b"other".to_vec())) - .mount(&server) - .await; - Mock::given(method("GET")) - .and(path("/missing.tgz")) - .respond_with(ResponseTemplate::new(404)) - .mount(&server) - .await; - Mock::given(method("GET")) - .and(path("/broken.tgz")) - .respond_with(ResponseTemplate::new(500)) - .mount(&server) - .await; - let expected = sha512_sri_of(BODY); - - let gz_probe = probe_of(&server, "/gz.tgz").await; - assert_eq!( - gz_probe.failure(&expected).as_deref(), - Some("content-encoding gzip") - ); - assert_eq!( - gz_probe.sha512, - Some(sha512_sri_of(&gz)), - "the body is hashed as received, never decoded" - ); - assert_eq!( - probe_of(&server, "/other.tgz") - .await - .failure(&expected) - .as_deref(), - Some("sha512 mismatch") - ); - assert_eq!( - probe_of(&server, "/missing.tgz") - .await - .failure(&expected) - .as_deref(), - Some("http 404") - ); - assert_eq!( - probe_of(&server, "/broken.tgz") - .await - .failure(&expected) - .as_deref(), - Some("http 500") - ); - let refused = - fetch_artifact_probe(api(None).plain_http(), "http://127.0.0.1:9/a.tgz").await; - assert!( - refused - .failure(&expected) - .is_some_and(|r| r.starts_with("fetch error ")), - "{refused:?}" - ); - let scheme = fetch_artifact_probe(api(None).plain_http(), "file:///etc/passwd").await; - assert!(scheme - .failure(&expected) - .is_some_and(|r| r.starts_with("fetch error "))); - } - - #[tokio::test] - async fn sends_vlt_accept_encoding_and_no_authorization() { - let server = MockServer::start().await; - Mock::given(method("GET")) - .respond_with(ResponseTemplate::new(200).set_body_bytes(BODY)) - .expect(1) - .mount(&server) - .await; - let urls: BTreeSet = [format!("{}/a.tgz", server.uri())].into(); - let probes = probe_artifacts(&api(Some("sktsec_secret_token")), &urls).await; - assert_eq!(probes.len(), 1); - let requests: Vec = server.received_requests().await.unwrap(); - assert_eq!(requests.len(), 1); - assert_eq!( - requests[0] - .headers - .get("accept-encoding") - .map(|v| v.as_bytes()), - Some(VLT_ACCEPT_ENCODING.as_bytes()) - ); - assert!(requests[0].headers.get("authorization").is_none()); - assert!(requests[0] - .headers - .get("accept") - .is_none_or(|v| v.as_bytes() == b"*/*")); - } - - #[tokio::test] - async fn redirect_chains_of_ten_pass_and_eleven_fail() { - let server = MockServer::start().await; - for hop in 0..11 { - Mock::given(method("GET")) - .and(path(format!("/r{hop}"))) - .respond_with( - ResponseTemplate::new(302) - .insert_header("location", format!("{}/r{}", server.uri(), hop + 1)), - ) - .mount(&server) - .await; - } - Mock::given(method("GET")) - .and(path("/r11")) - .respond_with(ResponseTemplate::new(200).set_body_bytes(BODY)) - .mount(&server) - .await; - let expected = sha512_sri_of(BODY); - assert_eq!(probe_of(&server, "/r1").await.failure(&expected), None); - assert!(probe_of(&server, "/r0") - .await - .failure(&expected) - .is_some_and(|r| r.starts_with("fetch error "))); - } - - #[tokio::test] - async fn a_body_over_the_cap_is_a_fetch_error() { - let server = MockServer::start().await; - Mock::given(method("GET")) - .respond_with(ResponseTemplate::new(200).set_body_bytes(BODY)) - .mount(&server) - .await; - let url = format!("{}/big.tgz", server.uri()); - let probe = fetch_capped(api(None).plain_http(), &url, 4).await; - assert!( - probe - .failure(&sha512_sri_of(BODY)) - .is_some_and(|r| r.starts_with("fetch error ") && r.contains("too large")), - "{probe:?}" - ); - assert_eq!(probe.body, None); - } - - #[tokio::test] - async fn one_request_per_distinct_url() { - let server = MockServer::start().await; - Mock::given(method("GET")) - .respond_with(ResponseTemplate::new(200).set_body_bytes(BODY)) - .mount(&server) - .await; - let urls: BTreeSet = (0..6) - .map(|i| format!("{}/u{}.tgz", server.uri(), i % 3)) - .collect(); - let probes = probe_artifacts(&api(None), &urls).await; - assert_eq!(probes.len(), 3); - assert_eq!(server.received_requests().await.unwrap().len(), 3); - } - - fn dep(name: &str, sha512: Option<&str>) -> DepOverride { - DepOverride { - ecosystem: "npm".into(), - name: name.into(), - namespace: None, - version: "1.0.0".into(), - token: String::new(), - patch_uuid: format!("uuid-{name}"), - artifact_url: format!("https://patch.socket.dev/patch/npm/t/u/{name}-1.0.0.tgz"), - registry_override: None, - integrity: Integrity { - sha512: sha512.map(str::to_string), - ..Integrity::default() - }, - } - } - - fn lock(nodes: &[&str]) -> BTreeMap { - let body = nodes - .iter() - .map(|n| format!(" {n}")) - .collect::>() - .join(",\n"); - [( - VLT_LOCK.to_string(), - format!( - "{{\n \"lockfileVersion\": 1,\n \"options\": {{}},\n \"nodes\": {{\n{body}\n }},\n \"edges\": {{}}\n}}\n" - ), - )] - .into() - } - - #[test] - fn scope_needs_a_parsed_lock_a_sha512_and_a_default_instance() { - let files = lock(&[ - r#""~npm~a@1.0.0": [0,"a","sha512-old","https://registry.npmjs.org/a/-/a-1.0.0.tgz"]"#, - r#""~npm~b@1.0.0": [0,"b","sha512-B","https://patch.socket.dev/patch/npm/t/u/b-1.0.0.tgz"]"#, - r#""~custom~c@1.0.0": [0,"c"]"#, - ]); - let deps = [ - dep("a", Some("sha512-A")), - dep("b", Some("sha512-B")), - dep("c", Some("sha512-C")), - dep("d", Some("sha512-D")), - dep("a", None), - ]; - let none = BTreeSet::new(); - let scope = preflight_scope(&files, &deps, &none); - let got: Vec<(&str, bool)> = scope - .iter() - .map(|d| (d.patch_uuid.as_str(), d.already_pinned)) - .collect(); - assert_eq!(got, [("uuid-a", false), ("uuid-b", true)]); - assert!(preflight_scope(&BTreeMap::new(), &deps, &none).is_empty()); - let mut bom = files.clone(); - bom.insert(VLT_LOCK.into(), format!("\u{feff}{}", files[VLT_LOCK])); - assert!(preflight_scope(&bom, &deps, &none).is_empty()); - } - - #[test] - fn a_vlt_vendored_takeover_is_scoped_through_its_vendored_node() { - let files = lock(&[ - r#""file~.socket+vendor+npm+11111111-2222-4333-8444-555555555555+e-1.0.0+node__modules+e": [0,"e",null,".socket/vendor/npm/11111111-2222-4333-8444-555555555555/e-1.0.0/node_modules/e"]"#, - r#""file~.socket+vendor+npm+11111111-2222-4333-8444-555555555555+f-1.0.0+node__modules+f": [0,"f",null,".socket/vendor/npm/11111111-2222-4333-8444-555555555555/f-1.0.0/node_modules/f"]"#, - ]); - let deps = [dep("e", Some("sha512-E")), dep("f", Some("sha512-F"))]; - assert!(preflight_scope(&files, &deps, &BTreeSet::new()).is_empty()); - let vendored = BTreeSet::from(["uuid-e".to_string()]); - let scope = preflight_scope(&files, &deps, &vendored); - assert_eq!( - scope, - [PreflightDep { - patch_uuid: "uuid-e".into(), - artifact_url: "https://patch.socket.dev/patch/npm/t/u/e-1.0.0.tgz".into(), - sha512: "sha512-E".into(), - already_pinned: false, - vendored: true, - }] - ); - } -} diff --git a/docs/ecosystems.md b/docs/ecosystems.md index b03dc6665..4b5192f06 100644 --- a/docs/ecosystems.md +++ b/docs/ecosystems.md @@ -688,10 +688,9 @@ segment, or a URL segment equal to the lock's scalar `registry`) are redirected, peer and modifier variant of the `name@version` included. Instances under a named alias, a scoped registry or jsr stay untouched (`redirect_vlt_custom_registry_skipped`) and keep the run's `--vex` from attesting the package. `options` is never edited: vlt fails `vlt -ci` on any change there. Before anything is written, each artifact is fetched the way -vlt fetches it (`accept-encoding: gzip;q=1.0, identity;q=0.5`, raw body hashed); vlt -fails `EINTEGRITY` on a content-encoded response, so such a dependency is withheld -(`redirect_vlt_artifact_unverifiable`) instead of pinning a lock `vlt ci` cannot install. +ci` on any change there. vlt hashes the raw response body against the pinned sha512, so +the patch server sends artifacts unencoded (`Cache-Control: no-transform`); the scan +itself never downloads them. **Hosted: which lock confirms.** vlt drives a project when its install state (`node_modules/.vlt-lock.json` or `node_modules/.vlt/`) is present or no other npm-family diff --git a/docs/testing/hosted-production-e2e.md b/docs/testing/hosted-production-e2e.md index 5fd2b9e80..1eefd3173 100644 --- a/docs/testing/hosted-production-e2e.md +++ b/docs/testing/hosted-production-e2e.md @@ -51,16 +51,12 @@ the [vendored suite](vendored-production-e2e.md) together. ## vlt: the serve-encoding gate -vlt verifies the raw response body. If the patch server returns a content-encoded -artifact, the hosted CLI refuses it with `redirect_vlt_artifact_unverifiable` and -leaves the lock unchanged. The production leg probes the artifact using vlt's -request headers and asserts either that refusal or, for identity encoding, the -full fresh-checkout `vlt ci` proof. - -`SOCKET_PATCH_VLT_HOSTED_PRODUCTION_REQUIRED=1` requires the install branch and -fails on an encoded response. The -[serve watchdog](../../.github/workflows/vlt-serve-watchdog.yml) monitors this -boundary; the current run's probe, not a dated note, determines server behavior. +vlt verifies the raw response body, so a content-encoded artifact fails +`EINTEGRITY` at install time. The patch server sends archives with +`Cache-Control: no-transform`, and the hosted CLI pins them without downloading +them. The production leg asserts the pin and the full fresh-checkout `vlt ci` +proof. The [serve watchdog](../../.github/workflows/vlt-serve-watchdog.yml) +monitors the server's encoding. The [vlt guide](vlt-compatibility.md) defines the required leg reporting. ## Running @@ -81,7 +77,6 @@ coverage is required. | `SOCKET_PATCH_HOSTED_E2E_STRICT=1` | Fail instead of skipping missing required toolchains | | `SOCKET_PATCH_HOSTED_E2E_CANARY_STRICT=1` | Fail when a watched ecosystem gains a free fixture, prompting promotion | | `SOCKET_PATCH_VLT_E2E_JS` / `SOCKET_PATCH_VLT_E2E_VERSION` | Select an installed, version-pinned vlt executable | -| `SOCKET_PATCH_VLT_HOSTED_PRODUCTION_REQUIRED=1` | Require vlt's actual install proof rather than its encoding refusal | Tools include npm, Corepack for pnpm/Yarn, Bun, uv, Go, and Ruby/Bundler. The gem proof requires Bundler 2.6+ for lockfile checksums. vlt requires its configured diff --git a/docs/testing/vlt-compatibility.md b/docs/testing/vlt-compatibility.md index 1528da4ff..d46b31fe7 100644 --- a/docs/testing/vlt-compatibility.md +++ b/docs/testing/vlt-compatibility.md @@ -40,7 +40,7 @@ socket-patch alike. | Layer | Where | What it proves | |---|---|---| | Unit and golden | `socket-patch-core` lib tests; `tests/redirect_golden.rs` (`npm/vlt/*`, shared with depscan's TS rewriter), `tests/redirect_golden_reverse_replay.rs`, `tests/vlt_locks.rs` (captured locks of every era, and vendored wiring byte for byte against what real `vlt ci`, `vlt install --frozen-lockfile` and `vlt install ` keep on 1.2.0, 1.0.10, 1.0.4, 1.0.0-rc.32 and 1.0.0-rc.14) | DepID codec, collation (`tests/fixtures/vlt/collation-golden.json`), the node-line grammar, the hosted slot rewrite and its slot revert, vendored lock surgery and its inverse, lock inventory, VEX discovery | -| Hermetic suites | `in_process_redirect`, `in_process_vendor`, `in_process_rollback_hosted`, `in_process_vendor_bun_takeover`, `repair::repair_vendor_flavors_e2e`, `e2e_vex_lockfile`, … (the 3-OS `test` job) | every code of the vlt support, the artifact preflight against a wiremock server, the heal, takeovers, repair, the CLI surface | +| Hermetic suites | `in_process_redirect`, `in_process_vendor`, `in_process_rollback_hosted`, `in_process_vendor_bun_takeover`, `repair::repair_vendor_flavors_e2e`, `e2e_vex_lockfile`, … (the 3-OS `test` job) | every code of the vlt support, the heal, takeovers, repair, the CLI surface | | Real-vlt capstones | the five binaries below plus the vlt legs of `e2e_hosted_production` / `e2e_vendored_production` | real installs: `vlt ci` and frozen installs of patched locks, byte-stable locks, integrity enforcement, the heal on a warm tree, re-saves, upgrades, the hardlinked global store | | Native backtest | `scripts/backtest-vlt.py` | the production service end to end, per release, mode and project shape, against an oracle of the documented boundaries | @@ -61,11 +61,9 @@ asserted and each named test or row exists. `scripts/tests/test_ci_vlt_rows.py` (run by `lint-ecosystems`) pins the rows. - **Required:** `hosted-e2e` installs vlt 1.2.0 the same way, runs `vlt_pinned_matrix_production_hosted_install_proof` inside the production - suite (probe-driven: the clean refusal while patch.socket.dev re-encodes the - artifact, the full install proof once it does not) and - `vlt_pinned_matrix_production_vendored_install_proof`, both through the leg - checker. `SOCKET_PATCH_VLT_HOSTED_PRODUCTION_REQUIRED` stays unset until the - serve fix is verified. + suite (the scan pins the hosted artifact and a fresh checkout's `vlt ci` + installs it) and `vlt_pinned_matrix_production_vendored_install_proof`, both + through the leg checker. - **Advisory:** `.github/workflows/vlt-compatibility.yml` (path-filtered pull requests and pushes to main, nightly, and `workflow_dispatch` with `versions` / `shapes` / `modes`). A path-filtered workflow cannot be a @@ -111,11 +109,10 @@ python3 scripts/backtest-vlt.py --cli target/debug/socket-patch \ The backtest writes `captures/--/{result.json,cli-output.json,tree/,logs/}` and `summary.{json,md}`, and exits non-zero when any cell's verdict differs -from the oracle. While patch.socket.dev re-encodes the artifact, hosted cells -record `blocked-by-server-encoding`, and only when the cell's own probe saw a -non-identity `Content-Encoding` and the CLI refused cleanly. A blocked cell -must carry only `redirect_vlt_artifact_unverifiable`: the withheld dependency -never reaches a rewriter, so its lock-level and vendored codes are not due. +from the oracle. While patch.socket.dev re-encodes the artifact, must-patch +hosted cells record `blocked-by-server-encoding`, and only when the cell's own +probe saw a non-identity `Content-Encoding`. A blocked cell never runs the CLI +(vlt would fail `EINTEGRITY` on whatever it pinned), so no codes are due. `--identity-mirror` previews the hosted proof through a loopback mirror that fetches the artifact un-encoded (never an import source). `--serve-probe` is the watchdog's probe, `--canary-checks` the canary's watchdogs, and @@ -220,7 +217,7 @@ samples them per OS). | Binary | Suite | Legs | |---|---|---| -| `e2e_redirect_vlt_build` | `hosted` | `scan_fresh_ci`, `frozen_dead_registry`, `ordinary_install_stable`, `get_uuid_fresh_ci`, `tamper_cold_eintegrity`, `rollback_byte_exact`, `rerun_noop`, `warm_tree_invalidates`, `no_cleanup_stays_stale`, `heal_rule_b_hidden_lock_without_node`, `heal_rule_c_no_hidden_lock`, `heal_rule_c_no_record`, `scoped`, `peer_workspace_instances`, `peer_rekey_rollback`, `install_newdep_preserves`, `update_drops`, `resave_install_rollback`, `resave_crlf_rollback`, `resave_update_rollback`, `crlf_lock`, `mirror_registries_npm`, `scalar_registry`, `named_alias_untouched`, `scoped_registry_untouched`, `jsr_untouched`, `default_registry_alias`, `registry_from_env`, `registry_from_user_config`, `content_encoding_refused`, `old_lockfile_ignored`, `warm_cache_hazard`, `idempotence`, `manifestless_vex`, `ts_written_lock`, `optional_dependency_heal`, `then_vendored_optional_takeover`, `platform_optional_skipped` | +| `e2e_redirect_vlt_build` | `hosted` | `scan_fresh_ci`, `frozen_dead_registry`, `ordinary_install_stable`, `get_uuid_fresh_ci`, `tamper_cold_eintegrity`, `rollback_byte_exact`, `rerun_noop`, `warm_tree_invalidates`, `no_cleanup_stays_stale`, `heal_rule_b_hidden_lock_without_node`, `heal_rule_c_no_hidden_lock`, `heal_rule_c_no_record`, `scoped`, `peer_workspace_instances`, `peer_rekey_rollback`, `install_newdep_preserves`, `update_drops`, `resave_install_rollback`, `resave_crlf_rollback`, `resave_update_rollback`, `crlf_lock`, `mirror_registries_npm`, `scalar_registry`, `named_alias_untouched`, `scoped_registry_untouched`, `jsr_untouched`, `default_registry_alias`, `registry_from_env`, `registry_from_user_config`, `old_lockfile_ignored`, `warm_cache_hazard`, `idempotence`, `manifestless_vex`, `ts_written_lock`, `optional_dependency_heal`, `then_vendored_optional_takeover`, `platform_optional_skipped` | | `e2e_vendor_vlt_build` | `vendored` | `scan_fresh_ci`, `get_auto_fresh_ci`, `get_service_fresh_ci`, `durability`, `workspace_member_selfref`, `alias_selfref`, `peer_root_selfref`, `peer_member_selfref`, `single_peer_context`, `optional_warm_reinstall`, `dep_with_deps`, `hostile_gitignore`, `autocrlf_checkout`, `bin_bearing`, `package_json_devdeps_patch`, `repair_rebuilds`, `idempotency`, `revert_byte_exact`, `resave_install_revert`, `resave_uninstall_revert`, `resave_crlf_revert`, `dependency_bumped_rescan`, `dependency_uninstalled_rescan`, `tamper_planted_file`, `tamper_file_content`, `tamper_payload_package_json`, `tamper_symlink_outside`, `tamper_deleted_gitignore`, `tamper_lock_file_node_path`, `transitive_refused`, `legacy_lockfile_warning`, `absent_version_refused`, `lockless_reinstall`, `manifestless_vex` | | `mode_migration_vlt` | `migration` | `vendored_then_hosted`, `hosted_then_vendored`, `dry_run_parity`, `scoped_unwind_one_of_two`, `rollback_from_mixed`, `agent_apply_yields_to_vendored`, `agent_apply_after_hosted`, `hosted_scan_keeps_agent_patched_tree`, `agent_rollback_after_takeovers`, `pm_switch_npm_to_vlt`, `pm_switch_vlt_to_npm`, `flavor_changed`, `upgrade_hosted`, `upgrade_vendored` | | `e2e_safety_vlt` | `safety` | `linux_auto`, `explicit_hardlink`, `private_copies`, `cross_device_cache`, `agent_rollback`, `peer_fanout`, `hosted_heal`, `vendored_build`, `vendor_revert_and_repair`, `layout_note` | diff --git a/docs/testing/vlt-coverage.json b/docs/testing/vlt-coverage.json index bed95bfe9..990569b0e 100644 --- a/docs/testing/vlt-coverage.json +++ b/docs/testing/vlt-coverage.json @@ -59,17 +59,6 @@ "golden:npm/vlt/hidden-lock-sentinel", "golden:npm/vlt/hidden-lock-sentinel-only" ], - "redirect_vlt_artifact_unverifiable": [ - "scan_redirect_vlt_artifact_content_encoding_refused", - "scan_redirect_vlt_artifact_sha512_mismatch", - "scan_redirect_vlt_artifact_http_404", - "scan_redirect_vlt_artifact_http_500", - "scan_redirect_vlt_artifact_fetch_error", - "scan_redirect_vlt_artifact_already_pinned_failure_left_pinned", - "every_failure_reason", - "vlt_pinned_matrix_hosted_content_encoding_refused", - "vlt_pinned_matrix_production_hosted_install_proof" - ], "redirect_vlt_reinstall_required": [ "scan_redirect_vlt_heal_advisory_texts_exact", "vlt_hosted_remove_restores_and_emits_the_advisory", @@ -181,26 +170,6 @@ "vendor_vlt_reinstall_required: revert with a link still into the vendored dir": [ "a_revert_asks_for_a_reinstall_while_a_link_or_an_optional_spec_moves_back", "vendor_vlt_revert_asks_for_a_reinstall_of_a_linked_vendored_copy" - ], - "redirect_vlt_artifact_unverifiable: content-encoding": [ - "scan_redirect_vlt_artifact_content_encoding_refused" - ], - "redirect_vlt_artifact_unverifiable: sha512 mismatch": [ - "scan_redirect_vlt_artifact_sha512_mismatch" - ], - "redirect_vlt_artifact_unverifiable: http status": [ - "scan_redirect_vlt_artifact_http_404", - "scan_redirect_vlt_artifact_http_500" - ], - "redirect_vlt_artifact_unverifiable: fetch error": [ - "scan_redirect_vlt_artifact_fetch_error", - "scan_redirect_vlt_artifact_redirect_chain_11_fails" - ], - "redirect_vlt_artifact_unverifiable: offline": [ - "every_failure_reason" - ], - "redirect_vlt_artifact_unverifiable: left pinned by an earlier run": [ - "scan_redirect_vlt_artifact_already_pinned_failure_left_pinned" ] }, "modeCommand": { @@ -210,7 +179,7 @@ "vlt_pinned_matrix_hosted_scan_fresh_ci" ], "hosted/get": [ - "scan_redirect_vlt_artifact_get_uuid_driver", + "get_uuid_hosted_pins_vlt_without_fetching_the_artifact", "vlt_pinned_matrix_hosted_get_uuid_fresh_ci" ], "hosted/rollback": [ diff --git a/scripts/backtest-vlt.py b/scripts/backtest-vlt.py index 5c9df50ac..52226b294 100644 --- a/scripts/backtest-vlt.py +++ b/scripts/backtest-vlt.py @@ -31,13 +31,12 @@ survivesNoopInstall, repeatStableLock, rollbackByteIdentical and rollbackOriginalBytes. -Hosted cells whose expected verdict is `patched` first probe the artifact -(`accept-encoding: gzip;q=1.0, identity;q=0.5`, no decoding). While the -service re-encodes it, vlt would fail `EINTEGRITY`, so the only correct CLI -outcome is the clean refusal (`redirect_vlt_artifact_unverifiable`, nothing -written): the cell records `blocked-by-server-encoding`. That verdict is -never recorded without the probe seeing a non-identity `Content-Encoding`; -any other hosted failure is an error. +Hosted cells first probe the artifact (`accept-encoding: gzip;q=1.0, +identity;q=0.5`, no decoding). While the service re-encodes it, vlt fails +`EINTEGRITY` on whatever lock the CLI pins, so a cell whose expected verdict +is `patched` records `blocked-by-server-encoding` without running the CLI. +That verdict is never recorded without the probe seeing a non-identity +`Content-Encoding`; any other hosted failure is an error. Rows are written in the shape of depscan's capture-vlt.py `result.json` (`os, vlt, mode, shape, verdict, codes, manifestSha256, patches` plus the @@ -100,7 +99,6 @@ ACME_REGISTRY = 'https://registry.yarnpkg.com/' VLT_ACCEPT_ENCODING = 'gzip;q=1.0, identity;q=0.5' BLOCKED = 'blocked-by-server-encoding' -BLOCKED_CODES = ['redirect_vlt_artifact_unverifiable'] VERDICTS = ['patched', 'safe-refusal', 'unsafe', 'error', 'unsupported', BLOCKED] COMMAND_TIMEOUT = 600 VEX_PRODUCT = 'pkg:npm/vlt-patch-backtest@1.0.0' @@ -363,20 +361,18 @@ def observed_verdict(row): def required_codes(row): - """The codes the row must carry. A blocked cell's dep is withheld from - every rewriter (and a conversion never reaches its second run), so only - the refusal's own code is due, not the cell's lock-level or vendored - codes.""" + """The codes the row must carry. A blocked cell never runs the CLI, so + none are due.""" if row.get('verdict') == BLOCKED: - return BLOCKED_CODES + return [] return row.get('expectedCodes', []) def matches_expectation(row): """Observed == expected. Two allowances: a must-patch hosted cell whose - probe saw a content-encoded artifact and whose CLI refused cleanly - (BLOCKED), and a must-patch cell whose only unproven checks are the - documented vlt limitation (`expectedLimitation`).""" + probe saw a content-encoded artifact (BLOCKED), and a must-patch cell + whose only unproven checks are the documented vlt limitation + (`expectedLimitation`).""" if not set(required_codes(row)) <= set(row.get('codes', [])): return False verdict, expected = row.get('verdict'), row.get('expectedVerdict') @@ -1007,7 +1003,6 @@ def __init__(self, ctx, version, mode, shape_name): self.envelopes = [] self.fresh_patched = {} self.cli_failures = [] - self.preflight_failures = [] # CLI ------------------------------------------------------------------- def cli(self, args, cwd=None): @@ -1026,11 +1021,7 @@ def patch_run(self, mode, cwd=None): cwd = cwd or self.project verb = ['get', UUID] if self.spec.get('driver') == 'get' else ['scan'] code, out, err = self.cli([*verb, '--mode', mode, '--yes', '--cwd', cwd], cwd) - envelope = parse_envelope(out) - # A successful run's envelope is not on the row; keep the preflight's - # reasons there so the transport retry sees them. - self.preflight_failures.extend(envelope_details(envelope, BLOCKED_CODES[0])) - return code, envelope, err + return code, parse_envelope(out), err def revert(self): if self.mode == 'hosted': @@ -1154,13 +1145,13 @@ def run_cell(self): self.ctx['artifact_sha512'], self.case / 'probe') checks['serveEncodingIdentity'] = probe['verifies'] + if self.blocked_by_encoding(row, checks): + return self.finish(row, checks, started) if self.spec.get('before'): code, envelope, _ = self.patch_run(self.spec['before']) row['beforeExitCode'] = code save(self.case / 'before-cli-output.json', envelope) if code != 0 or snapshot(self.project) == pristine: - if self.blocked_refusal(row, [envelope], pristine): - return self.finish(row, checks, started) raise RuntimeError(f'the first {self.spec["before"]} run patched nothing ' f'(exit {code})') original = snapshot(self.project) @@ -1176,13 +1167,6 @@ def run_cell(self): row['changedFiles'] = sorted(n for n in set(original) | set(after) if original.get(n) != after.get(n)) row['manifestSha256'] = capture_tree(self.project, self.case / 'tree') - if self.mode == 'hosted' and self.blocked_refusal(row, [envelope], original): - return self.finish(row, checks, started) - if probing and 'serveEncodingIdentity' in checks and not checks[ - 'serveEncodingIdentity'] and expected == 'patched': - # The probe says vlt cannot verify the artifact, yet this was - # no clean refusal: whatever the CLI wrote is unsafe. - checks['cleanRefusalWhileEncoded'] = False if not checks.get('serveEncodingIdentity', True) and expected != 'patched': checks.pop('serveEncodingIdentity') status_ok = isinstance(envelope, dict) and envelope.get('status') in ('success', 'ok') @@ -1194,37 +1178,22 @@ def run_cell(self): row['error'] = tail(f'{type(error).__name__}: {error}', 4000) return self.finish(row, checks, started) - def blocked_refusal(self, row, envelopes, reference): - """A clean refusal of a must-patch hosted run while the cell's own - probe saw a content-encoded artifact.""" + def blocked_by_encoding(self, row, checks): + """A must-patch hosted cell whose own probe saw a content-encoded + 200: vlt cannot verify the artifact, so the cell is blocked.""" probe = row.get('serveProbe') or {} encoded = probe.get('status') == 200 and not encoding_is_identity( probe.get('contentEncoding')) if not encoded or row['expectedVerdict'] != 'patched': return False - details = [d for e in envelopes - for d in envelope_details(e, 'redirect_vlt_artifact_unverifiable')] - row['codes'] = sorted(set(row.get('codes', [])) | { - c for e in envelopes for c in envelope_codes(e)}) - clean = (snapshot(self.project) == reference - and any('content-encoding' in d and 'nothing was written' in d - for d in details)) - row['blockedDetails'] = details - if clean: - row['blocked'] = True - row['blockedByProbe'] = probe.get('contentEncoding') - row['expectedCodesUnblocked'] = row['expectedCodes'] - row['expectedCodes'] = BLOCKED_CODES - row.pop('manifestSha256', None) - row['manifestSha256'] = capture_tree(self.project, self.case / 'tree') - row['checks'].pop('serveEncodingIdentity', None) - return clean + row['blocked'] = True + row['blockedByProbe'] = probe.get('contentEncoding') + checks.pop('serveEncodingIdentity', None) + return True def finish(self, row, checks, started): if self.cli_failures: row['cliFailures'] = self.cli_failures - if self.preflight_failures: - row['preflightFailures'] = self.preflight_failures row['failingChecks'] = [k for k, v in checks.items() if v is False] row['notEvaluated'] = [k for k, v in checks.items() if v is None] row.setdefault('codes', []) @@ -1490,15 +1459,6 @@ def check_agent(self, row, checks, before_lock, vlt, original): # The CLI's own report of a transport failure: a request error, or a 5xx from # the patch API (e.g. "API request failed with status 504: error code: 504"). CLI_TRANSPORT_FAILURE = re.compile(r'error sending request for url \(|API request failed with status 5\d\d\b') -# The hosted vlt artifact preflight's fetch of the artifact failed in transit -# (vlt_preflight.rs `ArtifactProbe::failure`: "fetch error ", "http <5xx>"). -# The CLI withholds the dep, writes nothing and exits 0, so the cell reads as -# a safe refusal although the cell's own probe of the same URL verified. The -# fetch errors that are deterministic (an over-cap body, a non-http URL) and -# every other reason (content-encoding, sha512 mismatch, 4xx) stay final. -PREFLIGHT_TRANSPORT_FAILURE = re.compile( - r'vlt would fail to verify \S+: (?:http 5\d\d\b' - r'|fetch error (?!refusing a non-http)(?![^;]*too large))') # vlt's own registry fetch dropped (Node's errno codes for a connect or read # timeout, a reset connection and a temporary DNS failure). ENOTFOUND and # ECONNREFUSED stay final: a wrongly rewritten URL fails that way too. @@ -1516,8 +1476,7 @@ def transient(row): or probe.get('status') is None): return True text = json.dumps(row) - return any(p.search(text) for p in (CLI_TRANSPORT_FAILURE, PREFLIGHT_TRANSPORT_FAILURE, - VLT_TRANSPORT_FAILURE)) + return any(p.search(text) for p in (CLI_TRANSPORT_FAILURE, VLT_TRANSPORT_FAILURE)) def run_with_retries(cell_factory, job, attempts=3): @@ -1568,23 +1527,9 @@ def downgrade(args, ctx, vlt): try: vlt.run(['install'], project, out / mode / 'home', log) pristine = snapshot(project) - env, extra = ctx['cli_env'], [] - mirror = None - if mode == 'hosted' and not probe_artifact( - ctx['artifact_url'], ctx['artifact_sha512'], out / 'probe')['verifies']: - # The live artifact is content-encoded, so this build refuses to - # pin it; write the hosted pin through the identity mirror. - mirror = IdentityMirror().__enter__() - env = dict(env, SOCKET_PROXY_URL=mirror.url) - extra = ['--patch-server-url', mirror.url] - row['identityMirror'] = True - try: - code, output, _ = run([ctx['cli'], 'scan', '--mode', mode, '--json', '--yes', - '--no-telemetry', *extra, '--cwd', project], - project, env, log) - finally: - if mirror: - mirror.__exit__() + code, output, _ = run([ctx['cli'], 'scan', '--mode', mode, '--json', '--yes', + '--no-telemetry', '--cwd', project], + project, ctx['cli_env'], log) written = snapshot(project) ledger = '.socket/vendor/redirect-state.json' if mode == 'hosted' \ else '.socket/vendor/state.json' diff --git a/scripts/tests/test_backtest_harnesses.py b/scripts/tests/test_backtest_harnesses.py index 39e94625e..59480c04a 100644 --- a/scripts/tests/test_backtest_harnesses.py +++ b/scripts/tests/test_backtest_harnesses.py @@ -802,21 +802,19 @@ def test_matches_expectation(self): 'a spurious refusal of a must-patch cell fails') self.assertFalse(vlt.matches_expectation(self.row(expectedCodes=['x'])), 'a missing documented code fails') - refused = vlt.BLOCKED_CODES self.assertTrue(vlt.matches_expectation(self.row(verdict=vlt.BLOCKED, - blockedByProbe='gzip', codes=refused))) - self.assertFalse(vlt.matches_expectation(self.row(verdict=vlt.BLOCKED, codes=refused)), + blockedByProbe='gzip'))) + self.assertFalse(vlt.matches_expectation(self.row(verdict=vlt.BLOCKED)), 'blocked needs the probe to have seen a content-encoding') self.assertFalse(vlt.matches_expectation(self.row( - verdict=vlt.BLOCKED, blockedByProbe='gzip', expectedVerdict='safe-refusal', - codes=refused))) + verdict=vlt.BLOCKED, blockedByProbe='gzip', expectedVerdict='safe-refusal'))) limited = self.row(verdict='unsupported', vltLimitations=['x'], expectedLimitation='x') self.assertTrue(vlt.matches_expectation(limited)) self.assertFalse(vlt.matches_expectation(dict(limited, expectedLimitation=None)), 'only a documented limitation may leave a cell unproven') self.assertFalse(vlt.matches_expectation(dict(limited, failingChecks=['freshCi']))) - def test_a_blocked_cell_needs_only_the_refusal_code(self): + def test_a_blocked_cell_owes_no_codes(self): for version, mode, shape in (('0.0.0-16', 'hosted', 'direct'), ('0.0.0-32', 'hosted', 'direct'), ('0.0.0-32', 'vendored', 'hosted-then-vendored')): @@ -824,13 +822,10 @@ def test_a_blocked_cell_needs_only_the_refusal_code(self): expected = vlt.expected_codes(version, mode, shape) self.assertTrue(expected, 'the unblocked cell owes lock-level or vendored codes') blocked = self.row(verdict=vlt.BLOCKED, blockedByProbe='gzip', - expectedCodes=expected, - codes=['redirect_vlt_artifact_unverifiable']) - self.assertTrue(vlt.matches_expectation(blocked)) - self.assertFalse(vlt.matches_expectation(dict(blocked, codes=[])), - 'the refusal code itself is still due') - self.assertFalse(vlt.matches_expectation(dict( - blocked, verdict='patched', codes=['redirect_vlt_artifact_unverifiable'])), + expectedCodes=expected) + self.assertTrue(vlt.matches_expectation(blocked), + 'a blocked cell never ran the CLI') + self.assertFalse(vlt.matches_expectation(dict(blocked, verdict='patched')), 'a cell that ran still owes its codes') def test_a_warm_install_keeps_a_vendored_optional_copy_from_0_0_0_30(self): @@ -1132,22 +1127,6 @@ def test_only_transport_failures_retry(self): self.assertFalse(vlt.transient({'serveProbe': {'curlExit': 0, 'status': 200}, 'failingChecks': ['freshCi']})) - def test_a_preflight_transport_failure_retries(self): - def refusal(reason): - return {'serveProbe': {'curlExit': 0, 'status': 200}, 'safeRefusal': True, - 'preflightFailures': [f'vlt would fail to verify https://h//u/a.tgz: ' - f'{reason}; nothing was written for pkg:npm/a@1']} - for reason in ('http 502', 'http 504', 'fetch error error sending request', - 'fetch error no response within 60s', 'fetch error no response', - 'fetch error hosted artifact body not received within 300s'): - with self.subTest(reason=reason): - self.assertTrue(vlt.transient(refusal(reason))) - for reason in ('http 404', 'http 403', 'content-encoding gzip', 'sha512 mismatch', - 'offline', 'fetch error refusing a non-http(s) artifact URL', - 'fetch error hosted artifact too large (5 bytes > 4)'): - with self.subTest(reason=reason): - self.assertFalse(vlt.transient(refusal(reason))) - def test_a_vlt_network_drop_retries(self): for err in ("vlt install exited 1: Timeout { code: 'ETIMEDOUT', syscall: 'connect' }", 'vlt install exited 1: read ECONNRESET', 'getaddrinfo EAI_AGAIN registry', @@ -1158,24 +1137,6 @@ def test_a_vlt_network_drop_retries(self): with self.subTest(err=err): self.assertFalse(vlt.transient({'error': f'RuntimeError: {err}'})) - def test_a_successful_runs_preflight_reasons_reach_the_row(self): - with tempfile.TemporaryDirectory() as temp: - cell = vlt.Cell({'out': Path(temp), 'record': {}, 'cli': 'socket-patch', - 'cli_env': {}}, '1.2.0', 'hosted', 'direct') - cell.project.mkdir(parents=True) - row = dict(cell=cell.name, expectedVerdict='patched', passed=False, checks={}, - safeRefusal=True) - detail = 'vlt would fail to verify https://h/a.tgz: http 503; nothing was written for x' - envelope = {'status': 'success', 'events': [], 'warnings': [ - {'code': 'redirect_vlt_artifact_unverifiable', 'detail': detail}]} - with patch.object(vlt, 'run', return_value=(0, json.dumps(envelope), '')): - cell.patch_run('hosted') - with patch('sys.stdout'): - cell.finish(row, row['checks'], vlt.time.time()) - captured = json.loads((cell.case / 'result.json').read_text()) - self.assertEqual(captured['preflightFailures'], [detail]) - self.assertTrue(vlt.transient(captured)) - def test_a_transport_failure_reruns_from_scratch(self): with tempfile.TemporaryDirectory() as temp: rows = [{'cell': 'c', 'error': 'error sending request for url (https://x)'}, @@ -1197,75 +1158,40 @@ def run_cell(self): self.assertTrue((Path(temp) / 'result.json').is_file()) -class VltBlockedRefusalTests(unittest.TestCase): +class VltBlockedByEncodingTests(unittest.TestCase): """`blocked-by-server-encoding` needs the cell's own probe to have seen a - content-encoded 200 and the CLI to have refused cleanly.""" - - DETAIL = ('vlt would fail to verify https://patch.socket.dev/x.tgz: content-encoding gzip; ' - 'nothing was written for pkg:npm/minimist@1.2.2') + content-encoded 200 for a must-patch cell.""" def setUp(self): self.temp = tempfile.TemporaryDirectory() self.addCleanup(self.temp.cleanup) self.cell = vlt.Cell({'out': Path(self.temp.name), 'record': {}}, '1.2.0', 'hosted', 'direct') - self.cell.project.mkdir(parents=True) - (self.cell.project / 'vlt-lock.json').write_text('{"nodes":{}}\n', encoding='utf-8') - (self.cell.project / 'package.json').write_text('{}\n', encoding='utf-8') - self.reference = vlt.snapshot(self.cell.project) def row(self, encoding='gzip', status=200, expected='patched'): return dict(serveProbe={'status': status, 'contentEncoding': encoding}, - expectedVerdict=expected, expectedCodes=['redirect_vlt_old_lockfile_ignored'], - codes=[], checks={'serveEncodingIdentity': False}) + expectedVerdict=expected) - def envelope(self, detail=DETAIL): - return {'status': 'success', 'events': [], - 'warnings': [{'code': 'redirect_vlt_artifact_unverifiable', 'detail': detail}]} - - def test_a_clean_refusal_of_an_encoded_artifact_is_blocked(self): - row = self.row() - self.assertTrue(self.cell.blocked_refusal(row, [self.envelope()], self.reference)) + def test_an_encoded_artifact_blocks_a_must_patch_cell(self): + row, checks = self.row(), {'serveEncodingIdentity': False} + self.assertTrue(self.cell.blocked_by_encoding(row, checks)) self.assertTrue(row['blocked']) self.assertEqual(row['blockedByProbe'], 'gzip') - self.assertEqual(row['expectedCodes'], vlt.BLOCKED_CODES) - self.assertEqual(row['expectedCodesUnblocked'], ['redirect_vlt_old_lockfile_ignored']) - self.assertNotIn('serveEncodingIdentity', row['checks']) - self.assertEqual(sorted(row['manifestSha256']), ['package.json', 'vlt-lock.json']) + self.assertNotIn('serveEncodingIdentity', checks) + self.assertEqual(vlt.observed_verdict(row), vlt.BLOCKED) def test_an_identity_probe_is_never_blocked(self): for encoding in (None, '', 'identity'): with self.subTest(encoding=encoding): row = self.row(encoding=encoding) - self.assertFalse(self.cell.blocked_refusal(row, [self.envelope()], - self.reference)) + self.assertFalse(self.cell.blocked_by_encoding(row, {})) self.assertNotIn('blocked', row) - def test_a_changed_project_is_not_a_clean_refusal(self): - (self.cell.project / 'vlt-lock.json').write_text('{"nodes":{"x":[]}}\n', - encoding='utf-8') - row = self.row() - self.assertFalse(self.cell.blocked_refusal(row, [self.envelope()], self.reference)) - self.assertNotIn('blocked', row) - def test_a_non_200_probe_or_a_must_refuse_cell_is_not_blocked(self): for row in (self.row(status=503), self.row(status=None), self.row(expected='safe-refusal')): with self.subTest(row=row): - self.assertFalse(self.cell.blocked_refusal(row, [self.envelope()], - self.reference)) - self.assertNotIn('blocked', row) - - def test_the_refusal_must_name_the_encoding_and_write_nothing(self): - for envelope in (self.envelope('vlt would fail to verify x: sha512 mismatch; ' - 'nothing was written'), - self.envelope('content-encoding gzip'), - {'status': 'success', 'events': [], - 'warnings': [{'code': 'redirect_vlt_lock_unsupported', - 'detail': self.DETAIL}]}): - with self.subTest(envelope=envelope): - row = self.row() - self.assertFalse(self.cell.blocked_refusal(row, [envelope], self.reference)) + self.assertFalse(self.cell.blocked_by_encoding(row, {})) self.assertNotIn('blocked', row) @@ -1298,12 +1224,12 @@ def test_grant_selection_fails_closed(self): def test_envelope_codes_read_every_channel(self): envelope = {'status': 'error', 'error': {'code': 'c', 'message': 'm'}, 'warnings': [{'code': 'a', 'detail': ''}], - 'events': [{'action': 'skipped', 'errorCode': 'redirect_vlt_artifact_unverifiable', + 'events': [{'action': 'skipped', 'errorCode': 'redirect_vlt_lock_unsupported', 'details': {'mode': 'hosted'}}, {'action': 'skipped', 'errorCode': 'b', 'reason': 'prose with spaces', 'details': {'mode': 'vendored'}}]} self.assertEqual(sorted(vlt.envelope_codes(envelope)), - ['a', 'b', 'c', 'redirect_vlt_artifact_unverifiable']) + ['a', 'b', 'c', 'redirect_vlt_lock_unsupported']) if __name__ == "__main__": diff --git a/scripts/tests/test_ci_vlt_rows.py b/scripts/tests/test_ci_vlt_rows.py index cf3dd15af..0d6c93944 100644 --- a/scripts/tests/test_ci_vlt_rows.py +++ b/scripts/tests/test_ci_vlt_rows.py @@ -247,9 +247,6 @@ def test_hosted_e2e_proves_vlt_against_production(self): for exported in ("SOCKET_PATCH_VLT_E2E_JS=", "SOCKET_PATCH_VLT_E2E_VERSION=1.2.0", "SOCKET_PATCH_VLT_E2E_REQUIRED=1", "SOCKET_PATCH_HOSTED_E2E_STRICT=1"): self.assertIn(exported, setup) - text = "\n".join(hosted) - self.assertNotIn("SOCKET_PATCH_VLT_HOSTED_PRODUCTION_REQUIRED", text, - "stays unset until the serve fix is verified") self.assertIn("check-vlt-legs.py", step(hosted, "Run hosted-mode production e2e")) vendored = step(hosted, "Run vendored-mode production e2e (vlt)") self.assertIn("--test e2e_vendored_production", vendored) diff --git a/scripts/tests/test_vlt_coverage.py b/scripts/tests/test_vlt_coverage.py index e0868ace5..c432dc62b 100644 --- a/scripts/tests/test_vlt_coverage.py +++ b/scripts/tests/test_vlt_coverage.py @@ -19,9 +19,8 @@ "redirect_vlt_entry_vendored", "redirect_vlt_unsupported_lock_key", "redirect_vlt_custom_registry_skipped", "redirect_vlt_old_lockfile_ignored", "redirect_vlt_scalar_registry_ignored", "redirect_vlt_lockfile_version_missing", - "redirect_vlt_sibling_lockfiles", "redirect_vlt_no_lockfile", "redirect_vlt_artifact_unverifiable", - "redirect_vlt_reinstall_required", "vendor_vlt_transitive_unsupported", - "vendor_vlt_lock_out_of_sync", "vendor_vlt_legacy_lockfile", "vendor_vlt_reinstall_required", + "redirect_vlt_sibling_lockfiles", "redirect_vlt_no_lockfile", "redirect_vlt_reinstall_required", + "vendor_vlt_transitive_unsupported", "vendor_vlt_lock_out_of_sync", "vendor_vlt_legacy_lockfile", "vendor_vlt_reinstall_required", "vendor_flavor_changed", "vendor_artifact_gitignored", ] MODE_COMMANDS = [ @@ -154,8 +153,7 @@ def test_every_advisory_variant_maps_to_existing_tests(self): self.assertIn(key.split(":", 1)[0], CODES, key) self.assertTrue(names, key) self.check_names(names) - for code in ("redirect_vlt_reinstall_required", "redirect_vlt_artifact_unverifiable"): - self.assertTrue(any(k.startswith(code + ":") for k in variants), code) + self.assertTrue(any(k.startswith("redirect_vlt_reinstall_required:") for k in variants)) def test_every_mode_command_cell_maps_to_existing_tests(self): self.assertEqual(sorted(self.data["modeCommand"]), sorted(MODE_COMMANDS))