Skip to content

feat(vex): manifest-less VEX from hosted/vendored lockfiles + npm 12 allow-remote auto-config (v5) - #251

Open
Mikola Lysenko (mikolalysenko) wants to merge 9 commits into
mainfrom
feat/vex-lockfile-inventory
Open

Mikola Lysenko (mikolalysenko) wants to merge 9 commits into
mainfrom
feat/vex-lockfile-inventory

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Sep 24, 2026

Copy link
Copy Markdown
Collaborator

Branch feat/vex-lockfile-inventory: 9 logical commits on main @ 15a6ba6 (#247 + #248); 272 files, +93.7k / −8.3k. The verbatim lock_inventory split (commit 3) accounts for 5.4k of each side, and most of the rest is tests.

Summary

socket-patch vex, and the VEX that apply / scan / vendor --vex generate, now attest hosted and vendored patches with no .socket/manifest.json and no .socket/vendor ledgers. The patch wiring is read straight from the project's root lockfiles and package-manager configs. This covers:

A ledger record whose lockfile wiring is gone no longer attests, even under --no-verify. That tightening is the breaking part.

Also in this PR:

  • npm 12 hosted mode works out of the box. npm ≥ 12 defaults to allow-remote=none and refuses every hosted redirect (EALLOWREMOTE). The hosted run now writes allow-remote=all to the project .npmrc and warns. Explicit values are respected, there is an opt-out flag, and every reversal removes exactly what was added.
  • Seven rewriter bugs the real-toolchain matrices found (composer, gem, cargo v1, yarn 4.0.x, npm dual-lock, npm 6).

Rebased onto #247: what adapted

The branch was squashed onto 09956d9 and re-split by concern. The #247 contracts it now builds on:

Rebased onto #248: what adapted

The 7 commits were replayed onto 15a6ba6. Conflicts, per commit:

Shared readers: one lockfile traversal layer

Discovery and the lock inventory (vendor::lock_inventory, which backs the scan / get supplement, the vendored fetch inventory and repair's recovery) read each lockfile format through ONE reader. That reader yields every entry, Socket-owned ones included. The inventory's registry views drop the Socket-owned entries, and discovery's extractors classify and validate them. Each consumer still chooses its own files and does its own I/O: discovery reads every present file through its recognizing ctx (rule 11), and the inventory keeps its precedence chains.

lock_inventory is now a directory module with one file per format. Each file has three parts: a pure entry model, a file-selection section that only stats files, and the registry view. Architecture tests enforce three things:

  • that layering;
  • extractors read file content only through the ctx;
  • the npm-family extractors never call the grammar primitives the models wrap.
Format Shared reader
package-lock / npm-shrinkwrap lock_inventory::npm_lock_nodes
pnpm-lock.yaml / shrinkwrap.yaml / Rush lock_inventory::pnpm::pnpm_packages over the hosted rewriter's pnpm grammar (every key generation, CRLF included), classify_pnpm_key, rush_lock_rels
yarn classic / berry lock_inventory::yarn::{classic_entries, berry_entries}. One berry locator (parse_berry_locator), one cache-key and checksum rule, and one multi-descriptor key splitter, shared by the hosted rewriter, the vendored berry backend and discovery
bun.lock / bun.lockb lock_inventory::bun::bun_text_entries over the backends' fail-closed vendor::bun_lock_text line grammar; BunLockb::parse_packages (also used by bun_lock's vendor paths and the trust anchor)
Cargo.lock / .cargo/config* cargo_lock::locked_packages. cargo_config's [patch] and [registries] walks and effective-config probe, which the writers read through too
go.mod / go.sum go_mod_edit's read helpers (normalize_for_read, block_structure_error, hosted_module_uuid), go_sum_edit::go_sum_lines / is_h1_dirhash
Gemfile.lock / Gemfile vendor::gemfile_lock, a new model covering sections, checksums, remotes and lock names. vendor::gem::gem_declaration_any and discovery's source-block grammar, now also used for ledger liveness
composer.lock lock_inventory::composer_lock_packages. The composer writer's lock walks use its array index too
uv / pylock / poetry / pdm / Pipfile.lock / requirements / Hatch utils::python_lock (uv source fields, script-lock pairing), utils::poetry_lock, lock_inventory::pypi::parse_pipfile_lock, utils::hatch::dependency_specs. utils::requirements, a lexer lifted out of the vendored planner, now provides the exact-pin rule for the inventory, discovery and the planner. There is one hosted pypi URL grammar for the Pipfile.lock inventory and discovery
pom.xml / nuget.config / packages.lock.json two new readers, vendor::maven_pom and vendor::nuget_config; nuget_feed::nuget_lock_entries, shared by discovery and the feed writer
pins and purls utils::digest (SRI pin rule, hex digest shapes) and utils::purl's validating builders, shared by discovery, the inventory, ledger recovery and the rewriters

Discovery's own duplicates are gone as well. DiscoverCtx::locate classifies a lock location once for every extractor (vendored path, hosted uuid, decorated leaf). There is one Wired type, one set of vendor-dir predicates and one JSON / TOML parse-and-diagnose helper.

Ledger liveness is one rule, Discovery::{wires_package, vendor_entry_live, redirect_record_live}. Each call site holds it through LedgerLiveness: the sorted redirect-ledger files, plus the lock inventory, loaded lazily and at most once.

  • It gates vex attestations.
  • It drives scan's takeover classification, hosted_wiring_retained and redirectState.wiringLive.
  • It reads every lock's entry for a package. A PEP 723 script lock that resolves the package from PyPI therefore no longer vetoes the project lock's hosted entry.

Cargo crates.io provenance is an explicit LockfileEntry::source_kind; it is no longer inferred from the checksum variant. The CLI shares one purl splitter, one vendor-ledger lookup and one npm identity crawl across vex, scan and vendor.

Integration round (after #248). The refactor's second pass removed the last parallel walkers that the duplication maps had found. There are 16 fixups, all folded into commits 4–9:

  • bun discovery now uses the strict shared grammar instead of its own lenient JSONC reader;
  • the berry key splitter is shared by the rewriter, the vendored backend and discovery;
  • requirements exact pins and the Pipfile.lock hosted-URL grammar are each defined once;
  • pnpm entry fields are read once;
  • Gemfile liveness uses discovery's source-block grammar;
  • liveness reads every lock entry.

The same round moved the reason a patch was gated into warnings[], so --json keeps it (see "Behavior changes").

Line counts

Production lines are non-blank lines with tests, #[cfg(test)] items and golden harnesses excluded. "Before" is the pre-refactor head of this branch; "after" is e3d0ca9 (commit 8).

Area Before After Code only (no comments)
vex/discover/* 8,285 7,349 (−936, −11%) 6,120 → 5,223 (−897)
vendor/lock_inventory (main: 2,262 in one file) 2,166 2,696 (+530) 1,643 → 1,961
CLI vex_sources.rs + vex_consumed.rs 1,429 1,422 1,107 → 1,090
new shared readers (maven_pom, nuget_config, utils::digest) 566
every changed production file (74) 64,256 65,074 (+818) 48,696 → 48,922 (+226)

The refactor is not a net line reduction. Each format now has one traversal; the duplicated walkers are gone from discovery and the scanners. lock_inventory and the writer modules gained the entry models, which yield every entry kind rather than just the registry ones, plus their docs.

Equivalence proof (golden snapshots)

Before any code moved, the refactor recorded golden snapshots on the pre-refactor tree:

  • discovery output for 182 committed fixture projects plus every unit-test call;
  • a tap on every lock-inventory entry point;
  • the ledger-liveness verdicts;
  • the manifest-less vex CLI output (exit code, envelope and document) for every hermetic e2e call.

Every later step had to keep them byte-identical. Old-vs-new differential copies of each consolidated function also ran during the work. Comparing the final tree against those baselines:

Golden set Baseline files Byte-identical Changed
discovery 435 433 2, both bun (below)
lock inventory 514 512 2, the same bun tests
ledger liveness 435 433 2, the same bun tests
CLI vex 244 174 70, each ONLY by an added warnings[] array (checked by deleting every warnings key and comparing)

The only behavioral change is intended. A hand re-indented bun.lock used to be read by discovery's own JSONC parser, while every other command refused it. It is now lockfile_unparseable, the same as everywhere else (reformatted_jsonc_lock_is_still_read became reformatted_lock_is_refused_like_every_other_reader). The CLI warnings[] additions are the new gating-reason warnings. No existing test assertion was loosened.

That scaffolding was about 460k lines of generated JSON, so it is not committed. Commit 9 keeps a compact regression pin instead: discovery output for the 182 committed fixture projects, one JSON file per fixture family (13 files, about 3.8k lines). It is regenerated with SOCKET_PATCH_UPDATE_GOLDEN=1 cargo test -p socket-patch-core --lib vex::discover::testing::golden, and a fixture without an entry (or an entry without a fixture) fails.

Left separate on purpose:

  • validation that differs by design: discovery's root-anchored vendor paths and host allowlist versus the writers' ownership checks, discovery's strict go.mod block rule, and maven/nuget version handling;
  • the remaining writer-side scanners: the maven_repo / redirect pom regexes, the nuget_feed / redirect key harvesters, wired_vendor_integrity's line windows and vendored_entry_in_use. Moving them onto the shared readers changes writer behavior, so each needs its own commit with a reviewed golden diff (follow-up).

Design

  • Discovery (core, vex::discover/). It is read-only, never uses the network and never fails the run: a malformed file becomes a diagnostic. There is one extractor per PM family:

    • npm (package-lock / shrinkwrap, pnpm every lock generation plus Rush);
    • yarn classic and berry;
    • bun (bun.lock / bun.lockb);
    • cargo;
    • golang (go.mod / go.work plus sums);
    • pypi locks (uv, PEP 723 script locks, pylock, poetry, pdm);
    • other pypi sources (Pipfile.lock, requirements plus -r, Hatch / PEP 621 direct refs);
    • gem, composer, maven, nuget;
    • deno, which is explicitly empty.

    The result is Discovery { refs, diagnostics, recognized, unlocked_pins, elsewhere }. Every root file is read; there is no precedence chain, because the hosted rewriter edits every candidate it finds.

  • Fail-closed validation. Every value is committed, tamperable data:

    • Hosted URLs: only https://patch.socket.dev or the configured --patch-server-url origin, with no userinfo. The uuid is the last canonical path segment, because grant tokens can be uuid-shaped.
    • Vendored paths: root-anchored .socket/vendor/<eco>/<uuid>/… whose leaf names the entry's own artifact.
    • Pins, not definitions. A registry, index or source definition alone never makes a ref.
    • Contested locks. A lock that resolves the same name@version from a non-Socket source drops the ref (patched_ref_unattributable).
    • Lockless pins. A lockless cargo pin or exclusive nuget mapping (UnlockedPin) can keep a ledger record live but never creates a ref.
  • Record resolution (CLI, vex_sources.rs). Sources are tried in order: manifest, redirect ledger, vendor-ledger embedded record, then (online only) the patch API by uuid. Fetches run 10 at a time with get's 401/403 → public-proxy fallback.

    • The uuid the lockfile actually wires beats a stale manifest or ledger uuid.
    • The socket-patch.vendor.json marker is never a record.
  • Evidence.

    • Vendored: the committed artifact is hashed.
    • Hosted: the installed copy the build consumes is hashed. That means the Go replacement module, the Socket-registry cargo src dir, or maven's suffixed version, never a pristine sibling. Installed evidence wins.
    • With nothing installed, the lockfile integrity pin is the evidence, the same as in-run scan --mode hosted --vex.
    • Discovered refs bypass the Property 7 ecosystem filter.
  • Liveness gates. These run before hashing and are still enforced under --no-verify, which now skips only the hashing:

    • vendor_unwired / redirect_unwired: the ledger record's lockfile wiring is gone. Discovery is authoritative for every uuid a read file mentions, so a rejected mention keeps nothing alive.
    • wiring_conflict: the lockfiles wire one package to several patches.
    • record_mismatch: the record names a different package or uuid.
    • record_unavailable: offline, 404, refused or a transport error. The run continues.
  • Product auto-detect. Adds go.mod, composer.json, pom.xml, a single *.csproj and a single *.gemspec, after the existing probes.

npm 12 .npmrc allow-remote auto-config

The decision was: fix the .npmrc and warn; respect explicit values; add an opt-out flag; make it revertible.

  • Trigger. A scan --mode hosted / get --mode hosted run that leaves a root package-lock.json / npm-shrinkwrap.json carrying a granted hosted artifact URL. The run then ensures allow-remote=all in the project .npmrc:

    • It creates the file, or appends one line, preserving the BOM, CRLF and every other byte.
    • It records the edit in the redirect ledger as redirect_npmrc_allow_remote (created / added).
    • Every npm hosted run warns redirect_npm_allow_remote with the tradeoff: all admits every url-resolved dependency tree-wide, while each entry's sha512 pin stays enforced.
  • Explicit values are respected and named in the warning. Any explicit non-all value in:

    • the project .npmrc;
    • the user, global or builtin npm config, located the way @npmcli/config does;
    • an npm_config_allow_remote env var, which beats every .npmrc.

    A symlinked, unreadable or bare-CR .npmrc is left alone, with a manual-remedy warning.

  • Opt-out. --no-npm-allow-remote-config / SOCKET_NO_NPM_ALLOW_REMOTE_CONFIG.

  • Grammar. Follows npm's bundled ini exactly, measured on npm 12.1.0 and cross-checked against ini 6.0.0 / 7.0.0:

    • only the exact key allow-remote counts;
    • the last top-level assignment wins, and [section] bodies are not top-level;
    • a bare CR ends a line;
    • a section header only counts on the untrimmed line;
    • the value is case-sensitive.
  • Revertible. Every reversal removes exactly what was added, once no package-lock entry needs it:

    • the whole-ledger replay (a new NpmrcAllowRemote inverse);
    • the per-purl npm revert behind scoped rollback / remove / the vendored takeover ("last one out", same transaction);
    • the vendored-supersedes-hosted reconcile.

    A created file the user has since modified keeps the user's lines (redirect_npmrc_allow_remote_modified, surfaced by rollback, remove, vendor and the reconcile). A symlinked .npmrc refuses an unwind at plan time, before anything is written.

  • Safety. Never written on --dry-run; the dry run previews the write, including for a vendored → hosted takeover. Mode-preserving atomic writes create the stage with the destination's permission bits, so a 0600 token-bearing .npmrc is never staged world-readable.

Output conventions

The human output this branch adds uses #248's mechanisms:

  • The omission line is Consolidated terminal UI and CLI-wide output polish #248's Warning: omitting <purl> from VEX: <phrase> (<tag>), with phrases for the new reasons (record_unavailable, record_mismatch, vendor_unwired, redirect_unwired, wiring_conflict).

  • Omissions are sorted by purl, so stderr and the skipped events are deterministic.

  • Plan notes are sorted, deduplicated and start with a capital letter.

  • Counts use ui::plural. Record fetches show a ui::StatusLine (Fetching patch records... (n/total)), which is silent under --json / --silent.

  • Manifest-less VEX lines use Consolidated terminal UI and CLI-wide output polish #248's format_vex_written / format_vex_dry_run_skip.

  • The npm caveat prints as Warning (redirect_npm_allow_remote): ….

  • Failed VEX runs carry the discovery diagnostics into warnings[] (standalone, embedded and scan JSON envelopes). The reason a patch was gated is there too:

    • why a wired patch has no record: vex_record_offline, vex_record_not_found, vex_record_fetch_failed;
    • a stale-credential fallback to the public proxy: api_auth_fallback, the same text as get / scan;
    • vex_wiring_conflict, vex_record_superseded and vex_claim_unwired.

    A failed embedded --vex adds one vex_omitted per omitted patch to the host command's warnings[], and --silent lists the omissions under the error.

Per-PM × version support matrix

Every PM is supported in both hosted and vendored modes, except as noted. "Hermetic" means the wiremock-backed e2e_vex_lockfile_<pm> suite in the default test job on all 3 OSes. "Real-PM" means the gated suites in CI's e2e matrix, the dedicated matrix jobs or the per-PM compatibility workflows; every real-PM flow ends in the manifest-less VEX matrix, with these cells:

  • manifest absent;
  • ledgers deleted;
  • --offlinerecord_unavailable with zero requests;
  • reverted lock, never attested, with or without --no-verify;
  • legacy-manifest where vendored.
PM Hosted Vendored Hermetic suite Real-PM versions (CI)
npm ✓ (npm 12: auto .npmrc; npm 6: fails closed, redirect_npm_legacy_client) ✓ (npm 12 dual-lock: both locks rewired) e2e_vex_lockfile_npm, redirect_npm_allow_remote 6.14.18, 7.0.0, 7.24.2, 8.19.4, 9.0.0, 9.9.4, 10.9.9, 11.20.0, 12.0.0, 12.1.0 (npm-compatibility.yml)
pnpm e2e_vex_lockfile_pnpm e2e_redirect_pnpm_build (3 OS), pinned vendored matrix, pnpm-compatibility.yml, Rush sim
yarn classic e2e_vex_lockfile_yarn_classic 1.0.2, 1.6.0, 1.7.0, 1.9.4, 1.10.1, 1.22.22 (yarn-classic-matrix)
yarn berry ✓ (needs a resolutions mapping) e2e_vex_lockfile_yarn_berry 4.0.2, 4.1.0, 4.6.0, 4.12.0, 4.18.0 (+ macOS/Windows); 2.x/3.x refused lanes (yarn-berry-e2e)
bun e2e_vex_lockfile_bun 1.3.14 redirect/vendor; bun.lockb 1.0.36 / 1.1.45; mode_migration 1.1.45 / 1.2.23 (bun-compatibility.yml)
cargo e2e_vex_lockfile_cargo toolchains 1.82.0 / 1.93.1 / stable × Cargo.lock v1–v4 (+ macOS/Windows v1) (cargo-vex-matrix)
golang ✓ (+ go-patches) e2e_vex_lockfile_golang 1.18.10, 1.21.13, 1.24.13, 1.26.3 (go-compatibility.yml); go.work build suite
uv e2e_vex_lockfile_uv 16 releases, 0.1.45 → 0.12.17; uv backtest
poetry e2e_vex_lockfile_poetry 1.0.10, 1.1.15, 1.8.5, 2.0.1, 2.4.3 (poetry-compatibility.yml)
pdm e2e_vex_lockfile_pdm 1.4.5, 1.15.5, 2.7.4, 2.8.2, 2.25.9, 2.29.2 (lock 3.1 and 4.0–4.2 refused)
hatch e2e_vex_lockfile_hatch 1.0.0, 1.2.1, 1.9.7, 1.14.2, 1.18.1
pipenv e2e_vex_lockfile_pipenv 2022.12.19 → 2026.8.0
pip (requirements) e2e_vex_lockfile_pip pip 22–26
gem e2e_vex_lockfile_gem bundler 1.17.3, 2.1.4, 2.2.33, 2.5.23, 2.6.9, 2.7.2, 4.0.15, 4.0.21; docker vendored gem
composer e2e_vex_lockfile_composer composer 1, 2.2 LTS, 2
maven ✓ (jar + .sha1 required) e2e_vex_lockfile_maven 3.6.3, 3.8.9, 3.9.16, 4.0.0-rc-6
nuget ✓ (needs a packages.lock.json entry or an exclusive mapping) e2e_vex_lockfile_nuget dotnet 6, 7, 8, 9, 10
deno e2e_vex_lockfile_deno (negative) 1.46.3, 2.9.7 (agent mode + setup.manual only)

Cross-cutting suites: e2e_vex_manifestless_embedded, e2e_vex_redirect, e2e_vex_vendor, vex_e2e_common_selftest, and the core polyglot test (a root with every PM discovers the union).

Real-PM spot checks on the final code (before the whitespace-only fmt fixups and the test-only golden slimming; local macOS, all green):

PM Version(s) Result
npm 12.1.0 hosted + vendored capstones (e2e_redirect_npm_build, e2e_vendor_npm_build) 24/24
pnpm 10.34.5 pinned matrix: hosted and vendored 3/3
yarn berry 4.12.0, plus 2.4.3 / 3.8.7 refusal lanes 103/103 cells
yarn classic 1.22.22 65/65 cells
bun 1.3.14 hosted + vendored 26/26
cargo 1.93.1 lock v1 10/10, lock v4 10/10 (redirect, vendor, mode migration)
go 1.26.3 53/53 (build, hosted, workspace, vendored)
uv 0.11.19 (Python 3.12) hosted 14/14, vendored 18/18
bundler 4.0.15 hosted + vendored 25/25
maven 3.9.16 hosted and vendored builds 2/2
dotnet 8.0 nuget build 2/2

The uv leg needs SOCKET_PATCH_UV_E2E_PYTHON set to a Python ≥ 3.9. Without it, uv picked this machine's Python 3.8 and hosted_uv_export_pylock_manifestless_vex stopped at setup (pylock.toml requires >=3.9). That is an environment issue, not a product one.

Earlier rounds (local macOS, all green): after the first refactor pass, the same eleven PMs. After the #248 rebase, npm 12.1.0 and pnpm 10.34.5. After the #247 rebase, npm 10.9.9 / 12.1.0, pnpm, both yarns, cargo, go, uv and bundler. Before the rebases, the full per-version matrices above were run per PM. At this HEAD they run in CI.

Behavior changes

  • BREAKING (v5): a ledger record attests only while a lockfile still wires it, even under --no-verify / --vex-no-verify. Those flags now skip only the hashing.
  • BREAKING: a malformed or unreadable .socket/vendor/state.json is vendor_ledger_corrupt for vex (exit 2; the host command fails under --vex).
  • New: hosted npm runs write allow-remote=all to the project .npmrc (or respect and name an explicit value) and always warn redirect_npm_allow_remote. New flag --no-npm-allow-remote-config / SOCKET_NO_NPM_ALLOW_REMOTE_CONFIG. New ledger kind redirect_npmrc_allow_remote and warning redirect_npmrc_allow_remote_modified. Commit the .npmrc with the lock.
  • New skip reasons: record_unavailable, record_mismatch, vendor_unwired, redirect_unwired, wiring_conflict.
  • New run-warning codes: lockfile_unreadable, lockfile_unparseable, patched_ref_invalid, patched_ref_unattributable, vex_record_offline, vex_record_not_found, vex_record_fetch_failed, vex_wiring_conflict, vex_record_superseded, vex_claim_unwired, vex_omitted; also api_auth_fallback on vex.
  • The shared readers change a few edge cases, each named in the CHANGELOG:
    • Lock inventory:
      • a v1 Cargo.lock's [metadata] checksums now verify a crates.io fetch;
      • requirements.txt is read as pip's logical lines;
      • a wildcard pin (six==1.*) is not an exact version;
      • a Pipfile.lock hosted reference to an sdist, an http:// origin or a path-prefixed origin stays inventoried;
      • a CRLF pnpm-lock.yaml is inventoried;
      • a poetry.lock / pdm.lock / Cargo.lock that is not valid TOML contributes nothing.
    • Discovery: a hand re-indented bun.lock is lockfile_unparseable.
    • Vendored berry backend: a multi-descriptor key that mixes the patched package with another descriptor is refused as vendor_override_conflict.
    • scan liveness: a commented-out Gemfile source … do block no longer keeps a reverted record alive.
  • manifest_not_found (exit 2) now means no manifest AND nothing wired.
  • apply --vex / vendor --vex without a manifest now attest what the lockfiles and ledgers wire. A project with nothing wired keeps the calm exit 0 and removes a stale document. apply --check and --dry-run never generate.
  • Discovery also runs under --global / --global-prefix, because it gates the ledgers read from --cwd.
  • The standalone vendor embeds the patch record in every ledger entry.
  • Rewriter fixes (each pinned by a core regression test):
    • composer hosted drops a source block that precedes dist; otherwise composer 1 / 2.2 silently installed the pristine commit from git. A hand-ordered source that can't be dropped warns redirect_composer_source_kept.
    • gem hosted puts the GEM section in bundler's source order (frozen installs on bundler ≥ 4.0.19).
    • cargo hosted and vendored now handle a v1 Cargo.lock.
    • yarn 4.0.0–4.0.2 checksums keep the lock's own spelling (YN0028 otherwise).
    • npm 12 dual-lock vendoring, with the new warning vendor_npm_sibling_lock_unwired.
    • npm lockfileVersion 1 hosted warns redirect_npm_legacy_client.

Limitations and follow-ups

  • Root only. Not read:
    • nested workspace-member locks (Rush pnpm locks are the exception);
    • maven parent poms or submodules;
    • Gradle;
    • $CARGO_HOME or parent cargo configs;
    • user-level nuget configs.
  • Online fetch. A record must be fetched online when neither the manifest nor the ledgers are committed. --offline gives record_unavailable.
  • Bun < 1.3.10. A hosted entry re-saved without its digest attests only after install.
  • Unsupported shapes.
    • gem platform gems;
    • composer installed.json and COMPOSER=-renamed locks;
    • legacy same-GAV maven hosted repositories.
  • Lockless cargo / nuget pins only keep an existing redirect-ledger record alive; they never create a ref on their own.
  • npm allow-remote=all is tree-wide, which is npm's granularity; the per-entry sha512 pins stay enforced. A user-level allow-remote=none is respected, so npm 12 then refuses the hosted install until the user changes it (the warning says so).
  • Follow-ups:

Test evidence

Final local gate on feat/vex-lockfile-inventory (HEAD faf9ed3) (macOS arm64, CARGO_INCREMENTAL=0). The branch is based on origin/main = 15a6ba6 (re-fetched after the gate; main has not moved) and the tree is clean.

Check Result
cargo clippy --workspace --all-features -- -D warnings (CI's step) clean
cargo clippy --workspace --all-features --all-targets -- -D warnings clean at HEAD, and on every one of the 9 commits on its own (run on the pre-fmt chain; the fmt fixups only change whitespace)
cargo test --workspace --all-features --no-run builds
cargo test --workspace --no-fail-fast (vexctl v0.3.0 on PATH, SOCKET_PATCH_GO_E2E_REQUIRED=1, like CI) exit 0, 8,503 passed, 0 failed, 135 ignored, across 276 test binaries. core: 4,575 passed (30 binaries, 1 ignored; lib 4,134). cli: 3,928 passed (244 binaries, 133 ignored, which are the gated real-PM / network suites; lib 785). doc-tests: 0 run, 1 ignored. Run just before the whitespace-only fmt fixups; after them the touched suites were re-run (cli lib 785, coverage_fix_scan_hosted_dryrun_vendored, in_process_redirect_pipenv)
rustfmt every new file is formatted. In the 4 changed files where this branch had added unformatted hunks, those hunks were fixed (8 hunks, folded into their commits); the other hunks in those files are main's own, untouched as the repo rules require
golden snapshots see "Equivalence proof". After slimming commit 9: clippy --all-targets -D warnings clean; cargo test -p socket-patch-core 4,570 passed, 0 failed; cargo test -p socket-patch-cli --no-fail-fast 3,928 passed, 0 failed, 133 ignored (245 binaries)
scripts/release-lint.sh --sync-only OK (4.0.0 everywhere)
npm dispatch tests (node --test npm/socket-patch/bin/socket-patch.test.mjs) 4/4
pypi dispatch tests (python3 pypi/socket-patch/test_dispatch.py) 9/9
python3 -B -m unittest discover -s scripts/tests 15 OK, 1 skipped
shellcheck: --shell=sh scripts/install.sh, the release scripts, and the new scripts/{uv,yarn-berry,yarn-classic}-vex-matrix.sh clean
actionlint same 8 findings as main (pre-existing ci.yml shellcheck infos; one line number shifts)

Not run locally, left to CI: the Linux and Windows test legs, test-release, coverage, e2e-docker, hosted-e2e (production credentials), and the full per-version matrices.

How to review

The commits are meant to be read in order, oldest first. Each one builds on its own:

  1. 75128f4 fix(core): hosted/vendored rewriter fixes found by the real-PM matrices. Seven independent rewriter bugs, each with a core regression test. It is small and has no VEX code, so it is a good warm-up.
  2. 0c5d370 feat(hosted): auto-configure npm 12 allow-remote in the project .npmrc. Start at core/src/patch/redirect/npmrc.rs (the grammar, the plan and the unwind). Then read replay.rs / takeover.rs, then cli/src/commands/scan/hosted.rs and remove.rs. Tests: tests/redirect_npm_allow_remote.rs.
  3. c8a97de refactor(core/vendor): split lock_inventory into per-format submodules. A verbatim move of main's file. Only imports, visibility, module docs and section banners change, and git show --color-moved shows everything else as moved. It exists so that the next commit's inventory changes can be read per format.
  4. 85dd900 feat(core/vex): discover hosted and vendored patch references from lockfiles.
    • Start at core/src/vex/discover/mod.rs: the types, the orchestrator, DiscoverCtx::locate, contested locks, recognized, and the liveness rule with LedgerLiveness.
    • Then read one extractor per family; npm.rs and pypi_locks.rs are the largest. Also read patch::redirect::hosted_patch_uuid.
    • This is the security-sensitive surface. Look for anything that turns a definition, a foreign host or an out-of-root path into a ref.
    • The shared readers are the entry models in vendor/lock_inventory/*, plus vendor/{gemfile_lock, maven_pom, nuget_config, cargo_config, go_mod_edit, go_sum_edit, yarn_berry_lock, yarn_classic_lock, bun_lock_text}.rs and utils/{digest, purl, requirements, python_lock, hatch}.rs.
  5. df672b6 feat(vex): manifest-less VEX.
    • cli/src/commands/vex_sources.rs: the record view, the API fetch and the gating-reason warnings.
    • vex_consumed.rs: hosted evidence.
    • vex.rs: the liveness gates and the output.
    • The embedded paths in apply.rs / vendor.rs / scan.
    • Also vendor_record_is_unowned, the product probes, and scan/mod.rs's classify_overlap_takeover / hosted_wiring_retained_purls, which now use the core liveness rule through commands::discover_wiring and LedgerLiveness.
  6. a9796f2 test(cli): the suites. Mostly mechanical. Worth reading: tests/vex_e2e_common/ (the shared omission oracle) and one hermetic suite, for example e2e_vex_lockfile_npm.rs.
  7. 164f594 ci: the matrices and backtest steps. Covers ci.yml, the new npm / go / poetry compatibility workflows and the scripts/*-vex-matrix.sh drivers.
  8. e3d0ca9 docs. The CLI_CONTRACT.md sections "Manifest-less VEX (lockfile discovery)" and "Additive warnings", the README, the CHANGELOG [Unreleased] section and docs/testing/npm-compatibility.md.
  9. faf9ed3 test(vex): golden snapshot of discovery over committed fixtures. The harness is core/src/vex/discover/testing/golden.rs; the JSON under core/tests/fixtures/vex-discover-golden/ is generated, so skim it.

🤖 Generated with Claude Code


Note

High Risk
Changes security-sensitive attestation rules, hosted npm install policy (.npmrc), and lockfile parsing used by scan/vex across every ecosystem; extensive CI coverage mitigates but the behavioral surface is large.

Overview
This is a v5 breaking release centered on manifest-less OpenVEX: vex (and embedded --vex on apply / scan / vendor) can attest hosted and vendored patches by reading patch wiring from root lockfiles and configs, with records resolved from manifest, ledgers, or the API. Ledger liveness is tightened: redirect/vendor ledger entries attest only while a lockfile still wires that patch—even under --no-verify (hash-only skip)—and corrupt .socket/vendor/state.json becomes vendor_ledger_corrupt for vex.

Hosted npm on npm 12 auto-writes allow-remote=all to the project .npmrc (ledgered, revertible, opt-out via --no-npm-allow-remote-config), with grammar aligned to npm’s ini and explicit user/env overrides respected.

Core work adds vex::discover plus shared per-format lock readers (split lock_inventory, unified liveness with scan). CLI adds vex_sources / vex_consumed, embeds patch records in all vendor ledger entries, and fixes several hosted/vendored rewriters (composer git fallback, gem source order, cargo v1, yarn 4.0.x checksums, npm dual-lock).

CI/docs expand massively: per-toolchain e2e matrices (uv, poetry, pdm, maven, dotnet, bundler eras, yarn/cargo/npm/go workflows), Docker vendor suites for pypi, and README/CHANGELOG/CLI contract updates for the new VEX and npm 12 behavior.

Reviewed by Cursor Bugbot for commit faf9ed3. Configure here.

First of seven commits that land the manifest-less VEX branch
(feat/vex-lockfile-inventory) on top of #247. The branch's ~165-commit
history, built on the pre-#247 main 9489b18 with per-PM merge commits, was
squashed onto 09956d9 and re-split by concern; the full pre-rebase history
is preserved on branch backup/vex-lockfile-pre-rebase.

These are the product bugs the per-PM real-toolchain matrices turned up
while the manifest-less VEX suites were written. Each is independent of
VEX and pinned by a core regression test:

- composer (hosted): the rewriter drops the entry's `source` block when it
  immediately precedes `dist` (one `redirect_composer_dist` fragment edit
  spanning both, reverted byte-for-byte). Composer 1 and 2.2 LTS silently
  installed the pristine upstream commit from git whenever the hosted
  download failed; a hand-ordered source that cannot be dropped warns
  `redirect_composer_source_kept`. Golden fixture: `source-and-dist`.
- gem (hosted): the patch-registry `GEM` section is inserted in bundler's
  source order (sorted by remote), so a frozen `bundle install` on bundler
  >= 4.0.19 accepts the converged lock. The `basic` golden and the exact
  lock expectations move the Socket section first.
- cargo (hosted + vendored): a v1 `Cargo.lock` (checksums in `[metadata]`,
  dependents naming the crate by its full package id) is redirected and
  vendored correctly: the `[metadata]` line and every dependent's full-id
  reference follow the source, each fragment its own ledger edit, and
  `cargo --locked` accepts the result; the vendored detach/restore of a v1
  entry is byte-identical. `plan_cargo_lock` keeps #247's multi-source
  twin disambiguation (`Ambiguous`) and hoisted regexes; the block end now
  also stops at a trailing `[metadata]` / `[[patch.unused]]`, and the
  checksum is inserted after a block-final `source` line too.
- yarn berry (hosted + vendored): written checksums follow the lock's own
  spelling — yarn 4.0.0–4.0.2 spell `10c0` checksums as bare hex, so the
  prefixed form failed `yarn install --immutable` with YN0028.
- npm (vendored): npm 12 reifies from the `package-lock.json` it creates
  beside a committed shrinkwrap, so `vendor` now rewires every present npm
  lock (siblings first, primary last; an unrewirable sibling warns
  `vendor_npm_sibling_lock_unwired`), and the in-use/revert probes read
  every npm lock before deleting an artifact. `select_lockfile` reads
  through #247's guarded `read_regular_to_bytes`.
- npm (hosted): a lockfileVersion 1 redirect warns
  `redirect_npm_legacy_client` — npm 6 ignores a v1 lock's `resolved` and
  fails EINTEGRITY against the patched pin.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
npm >= 12 defaults to `allow-remote=none` and refuses (EALLOWREMOTE) every
lockfile entry whose tarball is not served by the configured registry —
exactly what a hosted redirect writes. When `scan --mode hosted` / `get
--mode hosted` leaves a root package-lock.json / npm-shrinkwrap.json
carrying a granted hosted artifact URL, the run now ensures
`allow-remote=all` in the project `.npmrc` (creating the file, or
appending one line with the BOM, CRLF and every other byte preserved) and
records it in the redirect ledger as `redirect_npmrc_allow_remote`
(`created` / `added`).

- core `patch::redirect::npmrc`: npm's `.npmrc` grammar as measured
  against npm 12.1.0 (exact `allow-remote` key, last top-level assignment
  wins, `[section]` bodies are not top-level, bare-CR line splits, case-
  sensitive value), the plan (create / append / already-all / respected
  user / env / outer-layer value / unsupported), and the unwinds.
- Every reversal removes exactly what was added once no package-lock
  entry needs it: the whole-ledger replay (a new `NpmrcAllowRemote`
  inverse, grouped with the npm lock kinds), the per-purl npm revert
  behind scoped rollback / remove / the vendored takeover ("last one
  out", same transaction, flushed after the lock through #247's shared
  `staged::flush_staged`), and the vendored-supersedes-hosted reconcile.
  A modified created file keeps the user's lines
  (`redirect_npmrc_allow_remote_modified`, surfaced by rollback, remove,
  vendor and the reconcile). A symlinked `.npmrc` refuses an unwind at
  plan time, before anything is written.
- Hosted run: the `.npmrc` edit rides `rewrite.files` / `rewrite.edits`,
  so it is written under #247's apply-lock window, after its whole-run
  SYMLINK GUARD, and only after the ledger persisted — never on
  `--dry-run` (which previews the write, also for a vendored → hosted
  takeover; the root locks are now read for such a preview so the pnpm
  `trustLockfile` preview sees the lock the wet run splices). An explicit
  user value (project `.npmrc`, user/global/builtin config, or an
  `npm_config_allow_remote` env var) is respected and named; a symlinked,
  unreadable or bare-CR `.npmrc` is left alone; `--no-npm-allow-remote-
  config` / `SOCKET_NO_NPM_ALLOW_REMOTE_CONFIG` opts out. Every variant
  warns `redirect_npm_allow_remote` with the whole-tree tradeoff.
- `atomic_write_bytes_preserving_mode` creates its stage with the
  destination's permission bits (kept inside #247's `commit_stage`
  structure), so a 0600 token-bearing `.npmrc` is never staged world-
  readable.
- remove: the hosted leg's advisories are printed inside #247's
  `unwind_hosted` (so a run that then fails still reports them) and carried
  into the success envelope's `warnings[]`.

Tests: npmrc/replay/takeover/scan unit tests, the flag's parse coverage,
`redirect_npm_allow_remote` (plus #247's invariants: the lock never
outlives the run, dry runs create no `.socket/`, a full rollback leaves no
`.socket/` and never the user's `.npmrc`), and the dry-run takeover
previews in `coverage_fix_scan_hosted_dryrun_vendored`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A verbatim move with no behavior change, so the next commit can give each
lock format its shared entry model beside its registry view:
`vendor/lock_inventory.rs` becomes a directory module with one file per
format (`npm`, `npm_family`, `pnpm`, `yarn`, `bun`, `cargo`, `golang`,
`composer`, `gem`, `pypi`), ledger recovery (`recover`) and the rewired-lock
trust anchor (`wired`). `mod.rs` keeps the public API (`LockIntegrity`,
`LockfileEntry`, `UnsupportedNpmLayout`, `lookup`,
`inventory_project(_diagnosed)`, `recover_lock_entry`,
`wired_vendor_integrity`); the three test modules move to `tests.rs`,
`recover_tests.rs` and `python_lock_union_tests.rs`.

Only imports, visibility (helpers another file calls become `pub(super)`),
sibling-module paths (`super::state` -> `crate::vendor::state`), module
docs and the single-file section banners changed; the test modules lost
one indentation level and were re-wrapped by rustfmt. `git show
--color-moved` shows everything else as moved.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ckfiles

New read-only module `vex::discover`: `discover_patched_refs(_with)`
reads every supported ROOT lockfile / package-manager config and returns
the Socket patch wiring it finds — `Discovery { refs, diagnostics,
recognized, unlocked_pins, elsewhere }`, one `PatchedRef { purl, uuid, mode
(Hosted | Vendored), source_file, artifact_rel, locked_integrity,
integrity_required, url }` per live reference. It never touches the
network, never writes, and never fails the run: a malformed file is a
diagnostic (`lockfile_unreadable`, `lockfile_unparseable`,
`patched_ref_invalid`, `patched_ref_unattributable`).

One extractor per package-manager family: npm (package-lock /
shrinkwrap, pnpm every lock generation + Rush locks), yarn classic and
berry, bun (`bun.lock` / `bun.lockb`), cargo, golang (go.mod / go.work +
sums), pypi locks (uv, PEP 723 script locks, pylock, poetry, pdm), pypi
other (Pipfile.lock, requirements + `-r`, Hatch / PEP 621 direct refs),
gem, composer, maven, nuget; deno is explicitly empty. Every file present
is read — no precedence chain — because the hosted rewriter edits every
candidate it finds.

Every value is committed, tamperable data and is validated fail-closed:
- hosted: `hosted_patch_uuid` accepts only `https://patch.socket.dev` or
  a configured `--patch-server-url` origin, no userinfo, and takes the
  LAST canonical-uuid path segment (grant tokens may be uuid-shaped);
  percent-encoding, `\/` escapes and fragments are handled;
- vendored: root-anchored `.socket/vendor/<eco>/<uuid>/…` paths whose
  leaf names the entry's own artifact (`vendor_ref` takes the path
  literally; only yarn / URL-form pip strip their `#…` / `::…` decorations);
- pins, not definitions: a registry / index / source definition alone
  (cargo `[registries]`, nuget `<add>`, pom `<repository>`, uv index
  tables, `.npmrc`) never makes a reference;
- contested locks: a lock that resolves the same name@version from a
  non-Socket source drops the ref (`patched_ref_unattributable`);
- lockless cargo pins / exclusive nuget mappings are recorded as
  `UnlockedPin`s that can only keep a ledger record live, never create a
  ref; `recognized` lists every uuid a read file mentions, so a rejected
  mention keeps nothing alive downstream.

Supporting core changes: `patch::redirect::{SOCKET_PATCH_SERVER_HOST,
hosted_patch_uuid, hosted_patch_url_uuids}` (the pipenv owner check uses
the shared host constant); the pnpm lock grammar and `hosted_url_version`
exported crate-wide so readers parse exactly what the writers write;
`utils::digest` (the SRI pin rule and the hex digest shapes — one copy for
the inventory, discovery, ledger recovery and the rewriters, each call
site keeping its case policy); and `utils::purl`'s validating purl
builders, which discovery and the inventory's registry views share. A few
writer helpers become `pub(crate)` so the extractors' tests derive their
fixtures from the writers themselves.

One lockfile traversal layer: discovery and `vendor::lock_inventory` (the
scan / get / vendor / repair inventory) read each format through ONE
reader that yields every entry, Socket-owned ones included — the
inventory's registry views drop those, the extractors classify them:
- `lock_inventory` becomes a directory module, one file per format, each
  laid out as a pure entry model, a stat-only file-selection section and
  the registry view (an architecture test enforces the layering). Entry
  models: `npm_lock_nodes`; `pnpm_packages` over the hosted rewriter's
  pnpm grammar (every key generation, CRLF included); the yarn
  `classic_entries` / `berry_entries` models with one berry locator,
  cache-key and checksum rule; `BunLockb::parse_packages`; and
  `composer_lock_packages`, whose array index the composer writer's lock
  walks use too.
- Every other format reads through the reader its writer owns:
  `cargo_lock::locked_packages` and `cargo_config`'s `[patch]` /
  `[registries]` walks; `go_mod_edit` / `go_sum_edit` read helpers; a new
  `vendor::gemfile_lock` model; the `utils::python_lock` / `poetry_lock` /
  `hatch` readers (uv source fields, script-lock pairing, the pyproject /
  Hatch declaration walk); a new `utils::requirements` lexer lifted out of
  the vendored requirements planner; and two new XML readers,
  `vendor::maven_pom` and `vendor::nuget_config`, with
  `nuget_feed::nuget_lock_entries` shared by discovery and the feed
  writer.
- `DiscoverCtx::locate` classifies a lock location once (vendored path,
  hosted uuid, decorated leaf) for every extractor. The npm-family
  extractors iterate the entry models only, never the grammar primitives,
  and every extractor reads content only through the recognizing ctx
  (rule 11) — both enforced by architecture tests.

Ledger liveness is one rule too: `Discovery::{wires_package,
vendor_entry_live, redirect_record_live}`, held per call site by
`LedgerLiveness` (the sorted redirect-ledger files, the lock inventory
loaded lazily at most once), which the CLI's vex and scan both use. Cargo
crates.io provenance is explicit (`LockfileEntry::source_kind`), not
inferred from the checksum variant.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…m lockfiles

`socket-patch vex` and the embedded `apply` / `scan` / `vendor --vex` now
attest hosted and vendored patches with no `.socket/manifest.json` — and
with no `.socket/vendor` ledgers either — by reading the wiring out of the
project's lockfiles (`vex::discover`). This covers a depscan-opened PR, a
clone that never committed its ledgers, and a lock-only CI checkout.

Record view (`commands::vex_sources::plan`): four sources — the manifest,
the redirect ledger's records, the vendor ledger's embedded records and
lockfile discovery — merge into one view. A candidate's record must carry
the uuid the lockfile actually WIRES; it comes from the first source that
has one, else (online only) from the patch API by uuid, 10 fetches at a
time with `get`'s one-shot 401/403 → public-proxy fallback (#247's
uuid-only `ApiClient::fetch_patch`). Nothing is written: vex never takes
the apply lock, never creates `.socket/`, never writes the manifest, and
the `socket-patch.vendor.json` marker is never a record.

Gates, applied before hashing and kept under `--no-verify` (which now
skips only the hashing):
- a vendor-ledger entry attests only while a lockfile still wires its
  artifact (`vendor_unwired`); a redirect-ledger record only while a
  lockfile wires its hosted patch (`redirect_unwired`); discovery is
  authoritative for every uuid a read file mentions, so a rejected
  mention keeps nothing alive;
- `record_unavailable` (offline, 404, refused, transport error — the run
  continues), `record_mismatch`, `wiring_conflict` (lockfiles wire one
  package to several patches);
- a malformed / unreadable `.socket/vendor/state.json` is the hard error
  `vendor_ledger_corrupt` (exit 2), mirroring `redirect_ledger_corrupt` —
  this supersedes #247's degrade-and-disclose posture for `vex` only;
  `setup --check` keeps #247's `vendor_context_from` /
  `warn_unreadable_vendor_state` path.

Evidence: vendored refs hash the committed artifact (the ledger entry when
it names the wired artifact, else one synthesized from the ref); hosted
refs hash the installed copies the build CONSUMES
(`vex_consumed::hosted_consumed_copies` → core `VendorContext::hosted` /
`HostedCopies`: the Go replacement module, the Socket-registry cargo src
dir, maven's suffixed version — never a pristine sibling), and with
nothing installed a discovered pinned reference attests from its lockfile
pin, like in-run `scan --mode hosted --vex`. Discovered refs bypass the
Property 7 ecosystem filter.

Commands:
- `apply --vex` / `vendor --vex` with no manifest attest what the
  lockfiles and ledgers wire (nothing anywhere keeps the calm exit 0 and
  removes a stale document; `apply --check` and `--dry-run` never
  generate); #247's no-manifest lines ("No patch manifest found; nothing
  to apply.", "No manifest found, nothing to vendor.") are kept;
- failed VEX runs carry the discovery diagnostics into `warnings[]`
  (standalone envelope, embedded envelopes, scan JSON — hosted included);
- `manifest_not_found` now means no manifest AND nothing wired;
- human output: `Note:` lines for superseded records / fetch failures,
  phrased omission reasons.

Writer hardening: the manifest-driven standalone `vendor` now embeds the
patch `record` in its ledger entries too (vendored mode already does, as
`detached` entries). `detached` stays the "no manifest owner" flag, so
the manifest reconcile, legacy-manifest migration and get/scan
idempotency from #247 are unaffected. Every reader of embedded records
shares one ownership rule (`commands::vendor_record_is_unowned`): a
detached entry's record always stands alone, a standalone `vendor`
entry's fallback copy only when no manifest entry covers it (by ledger
key or base purl). `vex`, `list` and `setup --check`
(`fold_vendor_records`, formerly `fold_detached_records`) all apply it,
so one tree never lists "no patches" while its VEX document attests one.
`repair` stays narrower: it keeps preferring a manifest that moved on to
a newer uuid and falls back to the embedded copy only with no manifest
at all.

One liveness rule for vex and scan: `scan`'s cross-mode takeover
classification (`classify_overlap_takeover`), its
`hosted_wiring_retained` warning and `redirectState.wiringLive` ask the
same core discovery (`commands::discover_wiring`) and liveness rule
(`Discovery::redirect_record_live` / `vendor_entry_live` /
`wires_package`, through one `LedgerLiveness` holder per call site) that
gate attestation, replacing scan's private cargo / hosted / vendored
checks and its looser text scan. The CLI also shares one purl splitter
(`utils::purl::purl_parts`), one vendor-ledger lookup
(`vendor::state::lookup_entry_kv`) and one npm alias-aware identity crawl
(`ecosystem_dispatch::npm_paths_by_identity`) across vex, scan and
vendor, and pairs PEP 723 script locks through
`utils::python_lock::script_of_lock` like the rewriters do.

Output follows the conventions from #248: `ui::plural` counts, a
`ui::StatusLine` progress line for record fetches, and the shared
`format_vex_written` / `format_vex_dry_run_skip` lines on the
manifest-less `apply --vex` / `vendor --vex` paths. Manifest-less `vex`
honours `--dry-run` and `-O -` like the manifest path.

Product auto-detection adds go.mod, composer.json, pom.xml, a single
`*.csproj` and a single `*.gemspec`, after the existing probes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…pers

Hermetic suites (wiremock patch API, run in the default `test` job on
every OS): one `e2e_vex_lockfile_<pm>` per package manager — npm, pnpm,
yarn (classic + berry), bun, cargo, golang, uv, poetry, pdm, hatch,
pipenv, pip, gem, composer, maven, nuget, deno (negative) — covering
spoofed hosts, record mismatch, pristine / tampered installs, contested
and orphaned wiring, `--offline`, reverted locks (also `--no-verify`) and
the embedded commands; plus the cross-cutting `e2e_vex_manifestless_
embedded`, `e2e_vex_redirect`, `e2e_vex_vendor` and the
`vex_e2e_common_selftest` harness checks.

Real-PM capstones (gated like the existing suites, `_REQUIRED` env
pattern): every hosted / vendored build suite now ends in the manifest-
less VEX matrix over a fresh checkout — manifest absent (ledgers kept),
ledgers deleted (lockfile + API), offline (`record_unavailable`, zero
requests), reverted lock (never attested, verify or not) — through shared
helpers: `vex_e2e_common` (+ bun / uv matrices), `npm_e2e_common`
(+ `manifestless`), `yarn_berry_common`, `common/yarn_classic_vex`,
`common/bundler_e2e`, `cargo_e2e_matrix`, `golang_e2e_matrix`,
`maven_build_common`, `composer_e2e_common`, `vex_pdm_hatch_common`,
`vex_pipenv_pip_*`, `vex_pypi_real_common`. New real-toolchain suites:
`e2e_redirect_{composer,maven,uv}_build`, `e2e_vendor_maven_build`,
`e2e_golang_workspace_build`, `e2e_nuget_dotnet_build`,
`e2e_poetry_vex_build`, `e2e_vex_{pdm,pip,pipenv,hatch}_build`,
`e2e_deno_vex_build`; the docker / setup-matrix / production legs gain
their manifest-less VEX steps.

Re-pinned to #247 while rebasing:
- vendored mode writes no manifest: the "manifest deleted" steps are
  naturally manifest-free, so the npm manifest-less matrix and the uv /
  poetry vendored suites add a `legacy-manifest` cell (the record a
  pre-5.0 vendored run left beside its ledger, via
  `vex_e2e_common::seed_legacy_manifest`, must attest the same way), the
  yarn 2/3 refusal suite seeds that legacy record explicitly (and still
  requires `not_applied`, never an attestation), and `--detached` twins
  assert no manifest in either spelling;
- a fully reverted project keeps no `.socket/`: suites that plant a stale
  ledger back after a rollback recreate the directory first, and the
  hosted rollback suite checks that `vex` does not recreate it;
- the no-manifest human lines follow #247's wording;
- a corrupt vendor ledger on a manifest-free project is now
  `vendor_ledger_corrupt` (exit 2, the ledger named, never rewritten),
  replacing #247's disclose-then-`manifest_not_found` expectation;
- repair: two tests pin the standalone-`vendor` embedded record (a
  moved-on manifest still wins; with no manifest the embedded copy stands
  in offline), replacing the pre-rebase test that assumed a non-detached
  `scan --vendor` entry.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ci.yml:
- `test` exports `SOCKET_PATCH_GO_E2E_REQUIRED` / `_VERSION` so the
  real-go hosted/vendored suites fail instead of skipping on the Go that
  is already installed for vexctl;
- `e2e` gains pinned-toolchain legs, each ending in the manifest-less VEX
  matrix: composer 1 / 2.2 / 2 (hosted + vendored), bundler 1.17.3 →
  4.0.21 (hosted + vendored + setup_matrix_gem), bun text/binary-lock
  eras, the named corepack pnpm legs (3 OS), uv 0.1.45 → 0.12.x hosted +
  vendored, poetry / pdm / hatch / pipenv / pip, maven 3.6.3 → 4.0.0-rc-6,
  dotnet 6 → 10, deno; npm legs hard-require npm where
  `Command::new("npm")` can resolve it;
- new jobs: `yarn-classic-matrix` (1.0.2 → 1.22.22), `yarn-berry-e2e`
  (4.0.2 → 4.18.0 + macOS/Windows), `cargo-vex-matrix` (toolchains ×
  Cargo.lock v1–v4); `e2e-docker` also runs the pypi vendored-PM suite.

Compatibility workflows: npm (npm 6–12 capstones, new), go (1.18.10 →
1.26.3, new), poetry (new), and the pnpm / pdm / pipenv / bun ones run
the manifest-less VEX steps and trigger on the vex sources. The bun
workflow keeps #247's two modes (hosted, vendored — the vendored-
detached leg collapsed into vendored).

Scripts: the bun / pdm / pipenv / poetry / uv backtests gain the
manifest-less VEX checks (a fresh checkout of the committed state must be
attested, also with the ledgers deleted; `--offline` with no ledger omits
`record_unavailable`; a reverted lock never attests, also under
`--no-verify`; a refused lock format attests nothing — the pdm refusal
check follows #247's manifest-free footprint), plus the
`{uv,yarn-berry,yarn-classic}-vex-matrix.sh` drivers and harness unit
tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- CHANGELOG [Unreleased] (both entries are v5 MAJOR): the semver note now
  lists #247's changes and `vex` refusing stale ledger records and corrupt
  vendor ledgers; Changed (BREAKING) gains the ledger-liveness /
  `vendor_ledger_corrupt` entry (and #247's Fixed bullet that had `vex`
  disclose an unreadable ledger now points at it); Added gains the npm
  12 `allow-remote` auto-config, manifest-less VEX, standalone `vendor`
  record embedding and the new product probes; Fixed gains the composer /
  gem / cargo v1 / yarn 4.0 / npm dual-lock rewriter fixes after #247's
  own entries.
- CLI_CONTRACT.md: new "Manifest-less VEX (lockfile discovery)" section
  (inputs, per-ecosystem recognition table, record resolution,
  verification basis, liveness gates, run warnings), "Patch hosts", the
  embedded-VEX no-manifest rules, `manifest_not_found` for `vex`, the
  vendor ledger's `record` semantics (vendored-mode `detached` entries
  plus the standalone `vendor` fallback copy; the reconcile exemption keys
  on `detached`), and the new rollback warning code.
- README: "No manifest needed for hosted and vendored patches", the
  rewritten `vex` how-it-works steps and product probes, "npm
  compatibility (hosted mode and npm 12)", the hosted `.npmrc` commit
  hint, and the ledger-liveness note under "Undo things".
- docs/testing: npm-compatibility.md (npm 6–12, new), uv and bun tables,
  ecosystems.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Pin the exact output of lockfile discovery over every committed fixture
project, so a behavior change to discovery (or to the lock readers it
shares with the rewriters and vendor::lock_inventory) shows up as a golden
diff to review.

- Corpus: 182 projects under crates/socket-patch-core/tests/fixtures/ —
  every redirect case's input/ and expected/ tree, pnpm-hosted, poetry,
  pipenv, bun-lockb captures, and each pdm-native lock staged as pdm.lock —
  run through the full orchestrator and checked against rule 11's
  recognition-covers-refs invariant.
- Rendering (src/vex/discover/testing/golden.rs): every PatchedRef field
  (plus lockfile_basis_ok), diagnostics (tempdir root and OS error numbers
  normalized), recognized identities, unlocked pins, resolved-elsewhere
  entries, and the live hosted / vendored ledger claims. The destructuring
  is exhaustive, so a new field fails to compile until the golden covers it.
- Goldens: one JSON per fixture family (redirect-<eco>, bun-lockb,
  pdm-native, pipenv, pnpm-hosted, poetry) under
  tests/fixtures/vex-discover-golden/, mapping fixture path to output.
  Missing and orphaned entries fail.

Regenerate after an intended change with
SOCKET_PATCH_UPDATE_GOLDEN=1 cargo test -p socket-patch-core --lib
vex::discover::testing::golden. Unix only: Windows checkouts convert some
fixtures' line endings.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedgithub/​denoland/​setup-deno@​22d081ff2d3a40755e97629de92e3bcbfa7cf2ed97100100100100
Addedgithub/​actions/​setup-dotnet@​a98b56852c35b8e3190ac28c8c2271da59106c6898100100100100

View full report

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is ON. A cloud agent has been kicked off to fix the reported issue.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit faf9ed3. Configure here.

})
.collect();
all.extend(npm_paths_by_identity(options, &missing).await);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Workspace npm aliases skip VEX hashing

Medium Severity

Hosted npm VEX claims every consumed copy is hashed, but the alias walk only starts at the root node_modules and never visits workspace-member trees. The identity fallback that would find those aliases runs only when no copy was found at all, so a root or hoisted install causes member aliases to be skipped. A stale or tampered workspace alias can then stay unhashed while the document attests from the good copy.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit faf9ed3. Configure here.

Comment on lines +284 to +285
"{}: {what}: impact {impact:?} lacks {part:?}",
self.leg
let impact = st["impact_statement"].as_str().unwrap_or_default();
assert!(
impact.split("; ").any(|p| p == part),
"{leg} ({cell}): impact {impact:?} lacks {part:?}"
@socket-security-staging

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedgithub/​denoland/​setup-deno@​22d081ff2d3a40755e97629de92e3bcbfa7cf2ed99100100100100

View full report

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants