Skip to content

feat: three patch-apply modes — hosted, vendored, agent — with full ecosystem + VEX coverage#117

Merged
Mikola Lysenko (mikolalysenko) merged 30 commits into
mainfrom
feat/scan-redirect-mode
Jul 2, 2026
Merged

feat: three patch-apply modes — hosted, vendored, agent — with full ecosystem + VEX coverage#117
Mikola Lysenko (mikolalysenko) merged 30 commits into
mainfrom
feat/scan-redirect-mode

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

What

This PR ships the hosted patch-apply mode end to end and consolidates the CLI around three clearly-named modes, each fully VEX-attested and behaviorally tested across Socket's supported ecosystems:

Mode Mechanism Positioning
hosted (scan --mode hosted) Lockfiles/registry configs point ONLY the patched deps at Socket-hosted, integrity-pinned packages Minimal changes to user code; no CI changes; planned future GitHub-app default (small PRs)
vendored (scan --mode vendored) Patched artifacts committed under .socket/vendor/, lockfiles rewired locally Airgapped; no runtime dependency on Socket infrastructure
agent (scan --mode agent) .socket/manifest.json + blobs committed; the CLI applies in CI/postinstall The original method; fully supported; requires CI modification — new setups should prefer hosted or vendored

--mode is the documented selector; the legacy spellings (--apply, --vendor, --redirect) remain as aliases. The depscan companion PR (SocketDev/depscan#22093) carries the backend registry endpoints, byte-identical TS rewriters, the GitHub-app hosted mode (patchApplyMode: 'agent' | 'hosted'), and the live-environment harness.

Highlights

Hosted mode

  • Per-dependency rewriters for npm/pnpm/yarn-classic, pip requirements/uv, cargo, composer, gem, nuget, and maven (pom projects) — never a fall-through proxy. Gradle projects get a paste-able exclusiveContent snippet (build scripts are not machine-edited; settings-level repos are silently ignored under PREFER_PROJECT, so surgical edits can't be made safe).
  • golang hosted is a documented NO-GO (docs/design/golang-hosted-no-go.md): day-2 sumdb failures need uncommittable GOPRIVATE; replacement-module identity would force per-grant artifacts; the default GOPROXY chain would leak tokened URLs (or licensed bytes) to Google's public mirror. The loud warning points at vendored mode; the GOPROXY serve routes remain documented as an ephemeral-CI recipe.
  • Rewriters are idempotent (re-runs record no ledger growth); the redirect ledger (.socket/vendor/redirect-state.json, mode: "hosted") merges append-only and embeds patch records so post-install vex hash-verifies redirected patches.
  • NuGet packageSourceMapping correctness: adding a mapping now fans out a * catch-all to pre-existing sources (and seeds nuget.org when the sources list is empty) — without it, every OTHER package fails NU1100.

Vendored mode

  • New NuGet backend: flat-folder feed + relative-path source + lockfile contentHash recompute; dotnet restore --locked-mode gives a fail-closed NU1403 on cache collisions. Proven in a real Docker container (fresh checkout, cold caches, --network none).
  • New Maven backend: the vendored dir is a real file:// maven2 repository (jar + REAL upstream pom + sidecars); multi-module and gradle-only projects refuse loudly; warm-~/.m2 shadowing warned with a purge one-liner. Docker-proven offline with a transitive-dep artifact.
  • Fragment-level revert: per-purl removal excises only our wiring fragment, preserving sibling patches and post-vendor user edits (whole-file restore only on the byte-identical fast path).

VEX

  • All three provenance forms are attested and tested: plain (agent), (vendored), (redirected) (hosted) — per-mode cross-ecosystem matrices (8 official ecosystems + qualified PURLs) plus VEX legs in every real-environment capstone.

Test matrix (each cell a real test)

  • Agent: in-process apply for all 8 ecosystems; docker real-toolchain suites ×8, each now with a VEX leg.
  • Vendored: host capstones (npm/pnpm/yarn/cargo/golang/pypi/uv + new gem/composer, toolchain-gated + CI-pinned) and docker suites (pypi/gem/composer + new nuget/maven, wired into CI coverage).
  • Hosted: golden fixtures ×10 flavors byte-identical to the TS backend; real-install capstones for npm (direct-URL) and cargo (registry-protocol) with negative integrity legs; depscan-side install-verify incl. the new maven leg.

Docs

  • README "Choosing a patch mode" with the comparison table, an honest mode×ecosystem support matrix, VEX marker documentation, and per-ecosystem caveats (maven ~/.m2 shadowing, mirrorOf mirrors, nuget locked-mode). CHANGELOG + CLI_CONTRACT now cover --mode, hosted mode, and the ledger contracts.

Merge coordination

Merge this first; the depscan PR re-points its submodule to the squash-merged commit before landing.


Update: yarn-berry + bun hosted support, Rush monorepos, maven fail-closed hardening

yarn-berry + bun hosted rewriters (new, both twins byte-identical via shared goldens)

  • yarn-berry: lock-only surgery — resolution: "name@npm:<ver>::__archiveUrl=<encoded url>" + checksum: 10c0/<hex> from the precomputed berry cache-zip checksum. package.json untouched, so yarn install --immutable --check-cache passes; tamper is a hard YN0018. Requires yarn 4's cacheKey: 10c0 (compressionLevel 0) — anything else refuses loudly. Oracle-verified against real yarn 4.12 (yarn itself writes the identical encoding + checksum).
  • bun: text bun.lock v1 — registry 4-tuple rewritten to a URL 3-tuple with sha512 SRI; bun install --frozen-lockfile passes, tamper is a hard IntegrityCheckFailed. bun.lockb-only projects are auto-migrated by scan --mode hosted via the user's own bun (--save-text-lockfile --frozen-lockfile --lockfile-only, offline-capable, fail-closed on drift; recorded in the ledger).
  • New golden flavors npm/yarn-berry (9 cases) + npm/bun (8 cases); real-install hosted capstones (e2e_redirect_yarn_berry_build, e2e_redirect_bun_build) with tamper twins; docker image gains corepack yarn@4 with berry vendored/agent legs.

Rush (pnpm) monorepos

  • Hosted: scan --mode hosted discovers common/config/rush/pnpm-lock.yaml + subspace locks (the pnpm rewriter is now basename-generalized for nested lock paths); warns redirect_rush_repo_state_stale when repo-state.json is present (preventManualShrinkwrapChanges interplay). Hosted URLs survive Rush's copy-to-common/temp install; proven by a rush-sim capstone + a gated RUSH_E2E=1 real-rush leg.
  • Vendored: refuses loudly (vendor_rush_unsupported) — Rush's generated-workspace install model breaks relative file: wiring; the error routes to hosted mode.
  • Agent: works through project symlink farms (pinned by a rush-shaped apply test); repair on redirect-only projects is now an informational no-op instead of manifest_not_found.

Maven hosted: fail-closed via version suffixing + Trusted Checksums

  • Patched artifacts are now served as <version>-socket.<uuid8> — a version that exists ONLY on the Socket repository. A repo outage or tampered bytes is now a hard build failure (Maven consults Central for the suffixed GAV and cannot find it) instead of a silent fallback to the unpatched artifact. Base-version paths 404; no maven-metadata.xml is published so version ranges can never discover suffixed versions.
  • Rewriters pin direct deps to the suffixed version or insert a <dependencyManagement> pin for transitives; ${property} versions refuse per-dep. When the upstream pom is unavailable, serving falls back to same-GAV with an explicit redirect_maven_same_gav_fallback warning.
  • Defense-in-depth: rewriters emit .mvn/maven.config + .mvn/checksums/checksums.sha256 (Maven 3.9+ native Trusted Checksums; sha256-pins jar + pom, inert pre-3.9 where suffixing still holds). The strict contract is pinned by rewritten registry-redirect legs (tamper/outage MUST fail) and a trusted-checksums CI leg in the installer suite.

Also

  • Repair e2e matrix extended to pnpm/berry/bun (delete/corrupt/tamper/ledger-reconstruct); agent-mode installer legs for bun + berry(node-modules); README de-documents legacy --vendor/--apply spellings (--mode is the documented selector; flags remain as hidden aliases), support matrix + maven caveats rewritten to the fail-closed story.

Adds a new patch-apply mode that rewrites lockfiles/manifests so ONLY
patched dependencies resolve from Socket's hosted vendored patches
(patch.socket.dev), instead of vendoring local artifact bytes or
writing .socket/manifest.json.

- `scan --redirect` flag (conflicts_with_all apply/sync/vendor)
- `--patch-server-url` / `SOCKET_PATCH_SERVER_URL` global arg,
  defaulting to https://patch.socket.dev (DEFAULT_PATCH_SERVER_URL)
- api/client.rs `fetch_registry_references` (authed
  /v0/orgs/{org}/patches/package, or proxy /patch/package)
- patch/redirect/mod.rs rewriters for 9 ecosystems (npm package-lock,
  pnpm, yarn-classic, pypi requirements, uv, cargo, composer, nuget,
  gem); golang documented as a limitation (no per-dependency remote
  redirect without a global GOPROXY)

Shared golden fixtures (tests/fixtures/redirect/**) are consumed by
BOTH this crate's redirect_golden.rs and the depscan backend's
golden.test.ts, keeping the two rewriter implementations
byte-identical. Behavioral coverage: tests/in_process_redirect.rs.

Assisted-by: Claude Code:opus-4-8
@socket-security-staging

socket-security-staging Bot commented Jul 1, 2026

Copy link
Copy Markdown

All alerts resolved. Learn more about Socket for GitHub.

This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored.

View full report

@socket-security

socket-security Bot commented Jul 1, 2026

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: github shivammathur/setup-php is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: .github/workflows/ci.ymlgithub/shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore github/shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: github shivammathur/setup-php is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: .github/workflows/ci.ymlgithub/shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore github/shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: github shivammathur/setup-php is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: .github/workflows/ci.ymlgithub/shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore github/shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

…arker

`scan --redirect` previously dropped `--vex` silently and left no trail
VEX could attest: the redirect ledger carried only file edits, no patch
records, and the VEX verifier never read it.

- redirect ledger (`.socket/vendor/redirect-state.json`) now embeds the
  manifest PatchRecord (file hashes + vulnerabilities) per redirected
  PURL, fetched from the view endpoint during the redirect run; schema
  lives in patch/redirect/state.rs (RedirectState, load_redirect_state)
- vex: new augment_with_redirect folds ledger records into the manifest
  view (mirror of augment_with_detached) for both the standalone `vex`
  command and the embedded --vex paths, so redirected patches attest
  post-install against the installed tree exactly like `apply`
- build: build_document_with_provenance generalizes the vendored marker
  with a `(redirected)` impact phrasing; redirected PURLs bypass the
  property-7 ecosystem filter for the same reason vendored ones do (the
  committed lockfile rewrite is the persistence mechanism)
- scan --redirect --vex now emits the OpenVEX document in-run (the
  redirected bytes are remote pre-install, so the in-run attestation is
  built from the ledger records; a post-install `socket-patch vex`
  hash-verifies the installed tree)

Tests: e2e_vex_redirect.rs (installed-tree verify, property-7 bypass,
tampered→hash_mismatch fail-closed, no-verify ledger attestation,
cross-ecosystem incl. maven/nuget/composer + qualified PURLs),
in_process_redirect.rs --vex leg, core unit tests for the ledger
round-trip and the (redirected) phrasing, and real-toolchain VEX legs in
the npm/cargo/pnpm/yarn-classic/pypi/golang e2e_vendor_*_build capstones.

Assisted-by: Claude Code:claude-fable-5
Pure rustfmt reflows (rewraps, trailing commas, brace elision) — no
logic changes. Normalizes the #116 merge and this branch's test files
to plain `cargo fmt` output.

Assisted-by: Claude Code:claude-fable-5
…EX legs

e2e_redirect_npm_build.rs proves the full redirect chain with the real
toolchain: a mock patch server serves real patched tarball bytes;
scan --redirect rewrites package-lock.json; a fresh-checkout npm ci
(empty cache) installs the patched bytes from the served URL; a
post-install socket-patch vex HASH-VERIFIES the installed tree and
emits the (redirected) statement. The negative leg serves tampered
bytes against the real lockfile pin and asserts npm ci fails with
EINTEGRITY — the pin is enforcement, not decoration.

The docker vendor suites (gem via bundler, composer) gain a VEX leg
inside the existing stage-1 container run: the staged manifest now
carries a vulnerability, socket-patch vex runs in-container against the
vendored artifact, and a host-side oracle asserts the document (one
statement, not_affected, correct PURL subcomponent, "(vendored)"
marker). Both suites executed against real Docker containers.

Assisted-by: Claude Code:claude-fable-5
…faced record-fetch failures

Adversarial-review findings on the redirect->VEX wiring, each with a
pinned regression test:

- CRITICAL: the in-run --vex exemption trusted the whole redirect
  ledger, so a granted patch whose rewriter never touched a file (no
  lockfile present), a stale ledger record, or a manifest patch that
  failed verification could all be attested not_affected — suppressing
  a live CVE. run_redirect now derives a CONFIRMED set (the dep's
  hosted URL actually present in a project file after the rewrite):
  only confirmed deps are recorded in the ledger, exempted from in-run
  verification (VexBuildParams.assume_applied), counted as redirected,
  or allowed through the property-7 bypass. The no-lockfile case now
  warns, writes no ledger, and a requested attestation fails (exit 1).
- record-fetch failures (view endpoint down/404) after a successful
  redirect were silently swallowed, leaving the patch permanently
  unattestable with zero signal. They now surface as
  record_fetch_failed warnings in the JSON envelope and on stderr.
- five rewriters recorded an edit even when the entry was already at
  its target values, growing the committed ledger on every re-run and
  poisoning a future revert (npm entry, pnpm, Cargo.lock via a
  tri-state that keeps the pkg-not-found warning honest, nuget lock,
  gem CHECKSUMS). A second pass over rewritten output now records
  zero edits; the ledger merge keeps append semantics for real changes.
- requirements.txt: the environment marker was captured to end-of-line,
  swallowing a previously appended --hash and duplicating it on every
  re-run; markers are now taken from the requirement portion only.
  pip-compile --generate-hashes backslash continuations are refused
  with redirect_requirements_continuation instead of corrupted
  (orphaned --hash lines / mid-marker backslash = pip InvalidMarker).
- vendor::load_state treats a mode-tagged NON-vendor ledger squatting
  on .socket/vendor/state.json (an early depscan GitHub-app registry
  ledger) as empty instead of bricking remove/vendor/repair with
  vendor_state_unreadable; genuinely corrupt vendor ledgers stay
  fail-closed.

Assisted-by: Claude Code:claude-fable-5
The fixtures are a byte-for-byte cross-language contract; Windows
runners' CRLF checkout conversion made redirect_golden_fixtures_match
fail on the generated-vs-expected comparison. -text disables EOL
conversion for the fixture tree (contents are already committed LF).

Assisted-by: Claude Code:claude-fable-5
The redirect golden fixtures commit manifests/lockfiles that
deliberately pin old, vulnerable dependency versions — they are the
test inputs for a security-patching tool. Socket's PR scan flagged
their CVEs (Puma, Flask) as project dependency alerts; socket.yml now
scopes scanning away from the fixture trees.

Assisted-by: Claude Code:claude-fable-5
…ixes, golang NO-GO

- scan --mode <hosted|vendored|agent> is the documented mode selector;
  the legacy spellings (--redirect/--vendor/--apply) stay as aliases.
  Cross-mode combinations error; --detached composes with either
  vendored spelling. The hosted ledger writes mode: "hosted" (loader
  stays tolerant of pre-rename strings).
- rewrite_maven_pom: hosted maven support for pom projects — surgical
  <repository> insert (socket-patch-<uuid>, checksumPolicy=fail,
  snapshots disabled), same-GAV verify-only version check with
  unpinned/not-found/packaging warnings; gradle build files are never
  machine-edited (settings-level repos are silently ignored under
  PREFER_PROJECT) — a paste-able exclusiveContent snippet is emitted
  instead. Byte-identical to the TS twin via new shared maven/pom
  golden fixtures.
- nuget config correctness: creating a packageSourceMapping now fans a
  '*' catch-all out to pre-existing sources (else every other package
  fails NU1100), seeds nuget.org when the sources list is empty, and a
  self-closing <packageSources /> is expanded in place instead of
  leaving a duplicate element. New shared golden cases pin all three.
- golang hosted is a documented NO-GO (docs/design/golang-hosted-no-go
  .md): day-2 sumdb failures need uncommittable GOPRIVATE, replacement
  -module identity would force per-grant artifacts, and the default
  GOPROXY chain would leak tokened URLs or licensed bytes to a public
  mirror. The rewriter warning now names the decision and the remedy
  (vendored mode).

Assisted-by: Claude Code:claude-fable-5
…evert

- nuget_feed.rs: flat-folder feed at .socket/vendor/nuget/<uuid>/
  (relative-path <add> source resolves against nuget.config —
  committable), packageSourceMapping with the '*' catch-all rule,
  packages.lock.json contentHash = base64(sha512(nupkg)) recomputed
  locally. Service-first with local-cache fallback (deterministic
  re-zip of the patched extraction). dotnet restore --locked-mode
  turns cache collisions into a loud NU1403.
- maven_repo.rs: the vendored uuid dir IS a file:// maven2 repository
  (jar + REAL upstream pom + sha1/md5 sidecars — an authored minimal
  pom would drop transitive deps). Multi-module poms and gradle-only
  projects refuse loudly; warm ~/.m2 shadowing warned with a purge
  one-liner.
- Fragment-level revert for both backends: per-purl removal excises
  ONLY our wiring fragment, preserving sibling patches' wiring and
  post-vendor user edits; whole-file restore only on the byte-identical
  fast path; drift warns per the ledger contract.
- Shared plumbing: nuget/maven in ECOSYSTEM_DIRS + dispatch +
  leaf_to_purl; .nupkg/.jar handled by the zip verifier (VEX/repair);
  both cargo features promoted to defaults.

Assisted-by: Claude Code:claude-fable-5
- Agent mode: VEX legs in all 8 docker apply suites (real container
  apply → in-container vex → plain-marker statement asserted host-side);
  dedicated in-process apply tests for npm/composer/maven/nuget; a
  cross-ecosystem agent-mode VEX matrix (8 ecosystems + qualified
  PURLs) in e2e_vex.rs.
- Vendored mode: docker capstones for the new nuget (fresh-checkout
  cold-cache --network none restore, NU1403 tamper leg) and maven
  (offline mvn against the file:// repo, checksum negative, warm-cache
  shadow warning) backends; host capstones for gem/composer (toolchain-
  gated, CI-pinned via bundler 2.5 + setup-php); vendored VEX matrix in
  e2e_vex_vendor.rs including the new backends.
- Hosted mode: e2e_redirect_cargo_build capstone (wiremock sparse
  index + real patched .crate; fresh CARGO_HOME cargo fetch --locked;
  cksum tamper leg; post-install vex (redirected)); scan --mode parse
  contract tests.
- CI: docker vendor suites wired into the coverage-docker matrix;
  gem/composer host capstones pinned into the e2e job.

Assisted-by: Claude Code:claude-fable-5
README gains 'Choosing a patch mode' — the hosted/vendored/agent
comparison table (mechanism, user-code changes, CI requirements,
offline story, integrity story), an honest mode × ecosystem support
matrix derived from the code, per-mode walkthroughs, VEX provenance-
marker documentation (plain = agent, (vendored), (redirected) =
hosted), and per-ecosystem caveats (maven ~/.m2 shadowing + purge
one-liner, mirrorOf mirrors, nuget locked-mode). Agent mode is
positioned as fully supported but not recommended for new setups.
CHANGELOG covers hosted mode, --mode, the vendored backends, and the
nuget mapping fixes. CLI_CONTRACT adds scan --mode/--redirect and the
two ledger surfaces as contract.

Assisted-by: Claude Code:claude-fable-5
@mikolalysenko Mikola Lysenko (mikolalysenko) changed the title feat(cli): add scan --redirect hosted-vendored-patch mode feat: three patch-apply modes — hosted, vendored, agent — with full ecosystem + VEX coverage Jul 2, 2026
@socket-security

socket-security Bot commented Jul 2, 2026

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/​shivammathur/​setup-php@​f3e473d116dcccaddc5834248c8745238695824075100100100100

View full report

…t vendors

unsupported_ecosystem_purl_is_a_benign_skip used pkg:nuget as its
cannot-vendor ecosystem; the new NuGet vendored backend made that purl
vendorable (and the feature a default), so the fixture failed as
package-not-found instead of skipping benignly. pkg:jsr is the one
compiled-in ecosystem with no vendor backend by design — the contract
being pinned (benign skip, npm still vendors, exit 0) is unchanged.

Assisted-by: Claude Code:claude-fable-5
@socket-security-staging

socket-security-staging Bot commented Jul 2, 2026

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/​shivammathur/​setup-php@​f3e473d116dcccaddc5834248c8745238695824099100100100100

View full report

scan_vendor_flag_conflicts_are_clap_errors pins that flag-misuse errors
read like clap's own ('cannot be used with' / 'required'); the --mode
resolution errors said 'cannot be combined' and 'requires', failing the
contract on every CI OS. Exit code was already 2.

Assisted-by: Claude Code:claude-fable-5
A -C checksum failure on the Socket repository does not fail the build
when the GAV also exists on Maven Central — Maven falls back and
silently installs the unpatched artifact (the patch is dropped, though
mismatched bytes are never consumed). Surfaced by the depscan
install-verify negative leg; gradle dependency verification or vendored
mode remain the strong paths.

Assisted-by: Claude Code:claude-fable-5
…ralize pnpm redirect basename

Groundwork with no behavior change to existing single-lock flows.

Bun grammar: move the conservative `bun.lock` line grammar (BunEntry,
parse_packages_section, parse_entry_line, packages_bounds, scan_json_string,
scan_balanced_array, split_top_level, decode_json_string, split_name_spec,
check_lock_version, SUPPORTED_LOCK_VERSION) out of vendor/bun_lock.rs into a
new patch/bun_lock_text.rs. Fields on BunEntry become pub(crate) so both the
vendor classifier and lock_inventory can consume it. The vendor-specific
classify/TupleShape stay in bun_lock.rs. The grammar's unit test moves with
it. lock_inventory.rs now imports the grammar from the shared module.

URI: move encode_uri_component out of vendor/yarn_berry_lock.rs into a shared
utils/uri.rs as a pub fn. Its impl already matches JS encodeURIComponent
exactly (uppercase hex, `-_.!~*'()` unreserved), verified against yarn 4.12
with an added oracle vector.

pnpm redirect: generalize rewrite_pnpm_lock to rewrite every files-map key
equal to `pnpm-lock.yaml` or ending in `/pnpm-lock.yaml` (e.g. Rush's
common/config/rush/pnpm-lock.yaml), iterating keys sorted for stable goldens.
The per-dep entry-not-found warning now fires only when the dep matched in no
pnpm lock across the whole set. Adds the npm/pnpm/nested-rush-lock golden case
(shared by the Rust and TS suites). FileEdit.path and the output-files key are
the actual key.

Assisted-by: Claude Code:claude-fable-5
…ry fallback, redirect-only repair no-op

Rush keeps a single pnpm source-of-truth lock under common/config/rush/
(no root package.json/lock pair) and runs installs from a generated
common/temp workspace, so vendor's relative file: rewiring cannot survive.
Three disjoint pieces land the read/refuse side of hosted Rush support:

- npm_flavor: when no root lockfile matches and rush.json is present, the
  flavor probe refuses with vendor_rush_unsupported, naming the
  generated-workspace install model (overrides in pnpm-config.json,
  installs from common/temp) and routing to `scan --mode hosted`. The
  check fires only in the otherwise-missing arm, so a stray rush.json
  beside a real root lock still routes normally.

- lock_inventory: inventory_npm_lock (and thus inventory_project) falls
  back to the Rush locks when the root lock is absent but rush.json
  exists — the common source-of-truth lock plus every
  common/config/subspaces/*/pnpm-lock.yaml, read_dir sorted for
  determinism, repo-relative paths preserved.

- repair: a project whose only trace is .socket/vendor/redirect-state.json
  (hosted mode; no manifest, no vendor ledger, no vendored references) is
  a no-op for repair, not a manifest_not_found error. Exit success with an
  informational redirect_only_project skip routing to `scan --mode
  hosted`; contents are not validated.

Assisted-by: Claude Code:claude-fable-5
…red goldens

Adds `rewrite_yarn_berry` and `rewrite_bun_lock` to the registry-redirect
engine, the Rust half of the cross-language rewriter contract with the
depscan backend's TS `registry-rewrite/{yarn-berry,bun}` twins.

- Berry: rewrites ONLY the lock entry — `resolution:` gains yarn's own
  `::__archiveUrl=<encodeURIComponent(url)>` binding and `checksum:` becomes
  the override's `yarnBerry10c0` (berry verifies the converted cache zip, not
  the tarball). Descriptor key + package.json untouched, so `--immutable`
  still passes. Whole-file gates refuse a cacheKey != 10c0 or a
  `.yarnrc.yml compressionLevel != 0` (no offline-reproducible checksum).
- Bun: rewrites a registry 4-tuple `["name@ver","<reg>",{deps},"sha512-…"]`
  to a URL 3-tuple `["name@<url>",{deps verbatim},"<sha512>"]` using the
  shared `bun_lock_text` grammar (fail-CLOSED on any deviation). Binary
  `bun.lockb` is never parsed — presence without a text lock is a refusal.

Content-based dispatch: berry is detected by `^__metadata:` (classic already
declines it). New golden fixtures under `tests/fixtures/redirect/npm/{yarn-
berry,bun}/` (authored TS-side, byte-matched here); `RUST_IMPLEMENTED` gains
both. Warning-path unit tests cover every refusal branch.

Assisted-by: Claude Code:claude-fable-5
Wires the CLI's `scan --redirect` for the new berry/bun rewriters:

- `REDIRECT_CANDIDATE_FILES` gains `.yarnrc.yml` (berry cache-config gate)
  and `bun.lock`.
- The DepOverride merges the reference's `yarn-berry-zip` artifact —
  `yarnBerry10c0` into integrity and `berry_zip_url` from its URL — so the
  berry rewriter can pin the cache-zip checksum.
- Confirmed-redirect gating also matches `encode_uri_component(artifact_url)`,
  since the berry rewriter writes the URL percent-encoded into the lock's
  `::__archiveUrl=` binding (the raw form never appears there).
- bun.lockb auto-migration in `run_redirect`, before candidate reads: a
  binary lockfile with no text `bun.lock` is re-locked via `bun install
  --save-text-lockfile --frozen-lockfile --lockfile-only` (writes bun.lock,
  deletes bun.lockb, offline, fails closed). The removal is recorded as a
  ledger `FileEdit { action: "removed" }` (git history is the restore path);
  a dry-run only warns, and a failure degrades to the rewriter's own
  presence-only refusal.

In-process tests cover the berry leg (encoded __archiveUrl + yarnBerry10c0 +
preserved key + idempotent re-run), the bun leg (4-tuple → URL 3-tuple), and
the lockb migration via a fake `bun` shim on PATH.

Assisted-by: Claude Code:claude-fable-5
…te warning

Rush monorepos have no root package.json/lock pair: the single pnpm
source-of-truth lock lives at common/config/rush/pnpm-lock.yaml and, when
subspaces are enabled, one lock per subspace under
common/config/subspaces/<name>/. After the static REDIRECT_CANDIDATE_FILES
loop, run_redirect now reads those locks (when rush.json is present) into the
files map under their repo-relative keys — the pnpm rewriter is
basename-generalized, so nested locks are rewritten in place and the
path-generic write-back / confirmed-gating already handle them. Discovery of
the packages themselves flows through the lock_inventory Rush fallback landed
earlier.

When at least one Rush lock is read and common/config/rush/repo-state.json
exists, the run emits redirect_rush_repo_state_stale: editing the lock
outside `rush update` desyncs the pnpmShrinkwrapHash that file records, so
`rush install` fails until `rush update` refreshes it if
preventManualShrinkwrapChanges is enabled — but the redirect survives that
refresh (pnpm keeps locked resolutions for unchanged specifiers). repo-state
is Rush's to manage; the redirect never touches it.

Tests: a rush-shaped in_process_redirect fixture (rush.json + common lock +
subspace lock, no root pair) asserts both nested locks are repointed in
place; a subprocess JSON test pins the stale warning present-with-repo-state
and absent in the twin without it.

Assisted-by: Claude Code:claude-fable-5
Add `in_process_redirect_pnpm.rs`: the plain single-project pnpm
(lockfileVersion 9.0) root-lock counterpart of the npm hosted-redirect
legs. Asserts the `resolution:` splice to `{integrity: <patched>,
tarball: <hosted>}` (shared `npm/pnpm` golden shape), the
`redirect_pnpm_resolution` ledger edit, idempotency, and the `--vex`
`(redirected)` attestation.

Add `repair_vendor_flavors_e2e.rs`: generalize the npm-classic repair
invariants (`repair_vendor_e2e.rs`) across pnpm / yarn-berry / bun
vendored projects — deleted-tarball rebuild, corrupt-tarball rebuild,
tampered-ledger-sha fail-closed, and ledger-gone reconstruction from the
lockfile's vendored-tarball reference. Fixtures run the real
`scan --vendor` flow in-test against a mock API (no real package
manager) using each backend's pre-vendor lock shape.

Assisted-by: Claude Code:claude-fable-5
Extend `in_process_alternate_installers.rs` with three real-layout
agent-mode apply legs:

- bun: real `bun install` (private cache) → apply patches the hoisted
  node_modules copy;
- yarn-berry node-modules linker: `.yarnrc.yml nodeLinker: node-modules`
  + corepack-dispatched `yarn@4.12.0`, install → apply (complements the
  PnP refusal test in `e2e_safety_yarn_pnp.rs`);
- rush pnpm symlink farm: a hand-built
  `common/temp/node_modules/.pnpm/<pkg>@<v>/…` store with per-project
  `apps/{a,b}/node_modules/<pkg>` symlinks and `rush.json` at the root;
  apply at the root patches the canonical `.pnpm` file once and the
  bytes are visible through BOTH symlinks — pinning that the crawl finds
  the package via the project symlinks even though `common/temp` is in
  SKIP_DIRS.

Assisted-by: Claude Code:claude-fable-5
Overhaul the maven pom rewriter to consume the new server-side
version-suffixing contract. When a maven2 override carries
`mavenSuffixedVersion` + `mavenPomSha256`, pin the patched jar to the
Socket-only suffixed version explicitly — rewrite the literal `<version>`
or, for a transitive/managed dependency, author a `<dependencyManagement>`
entry — so an outage or tamper on the Socket repo hard-fails the build
instead of silently resolving the unpatched upstream artifact. A property
version is refused (a literal edit would break the reference); a mismatched
literal version is skipped. When a pin lands and both the jar and served-pom
sha256 are known, emit Maven Trusted Checksums files (`.mvn/maven.config`
resolver args + `.mvn/checksums/checksums.sha256` entries), merging into any
existing user config/checksum set (dedupe by key, never override a
conflicting value, sort checksums by path). Absent a suffixed version, fall
back to today's same-GAV repository injection with a
`redirect_maven_same_gav_fallback` warning (not fail-closed).

Add `maven_suffixed_version` + `maven_pom_sha256` to the identifiers struct,
list the two `.mvn/*` files in REDIRECT_CANDIDATE_FILES, and confirm a maven
redirect by the globally-unique suffixed version string (the `.pom` URL never
lands in a pom). The Gradle manual snippet now pins the suffixed version and
reminds the user to bump the declaration.

Update the shared golden fixtures (basic, existing-repositories,
property-version-warn now a zero-edit refusal, rerun-noop) and add new cases
(transitive-depmgmt, existing-depmgmt, no-suffix-fallback, mvn-config-merge,
version-mismatch-skip). The Rust rewriter is byte-identical to the TS twin.

Assisted-by: Claude Code:claude-fable-5
Add `e2e_redirect_yarn_berry_build.rs` and `e2e_redirect_bun_build.rs`,
the hosted-mode (`scan --mode hosted`) full-chain analogs of
`e2e_redirect_npm_build.rs` for the yarn-berry and bun flavors:
`scan --mode hosted` rewrites the lock to resolve the patched dep from
the wiremock hosted tarball, then a fresh-checkout install
(`yarn install --immutable --check-cache` / `bun install
--frozen-lockfile`, offline from the registry) MUST land the patched
bytes; a tamper twin serving different bytes must fail (YN0018 /
integrity).

berry derives the exact `10c0/<hex>` cache-zip checksum the mock hands
back by a bootstrap real-yarn `resolutions: file:` install (yarn
recomputes the same zip checksum for the archiveUrl locator). Two
harness robustness measures: the corepack gate probes from a neutral
tempdir (the monorepo root's `packageManager` field otherwise makes
corepack refuse yarn), and every yarn invocation sets
`YARN_ENABLE_GLOBAL_CACHE=false` so the persistent `~/.yarn/berry`
cache can't serve the tampered twin honest bytes.

`bun.lockb` migration is not exercised here (bun 1.3.x has no flag to
emit the binary lockfile); it stays covered by the in-process shim test
`scan_redirect_migrates_bun_lockb_then_redirects`.

Assisted-by: Claude Code:claude-fable-5
Dockerfile.npm: cache `yarn@4.12.0` in corepack WITHOUT `--activate` (the
global `yarn` stays classic 1.22.22); berry fixtures opt in per-project
via package.json `"packageManager": "yarn@4.12.0"`, which the corepack
shim dispatches. A build-time check pins that the shim resolves yarn 4
under a packageManager pin while the global stays classic.

docker_e2e_npm.rs: add two berry legs — an agent-mode install→apply
chain (container twin of the npm agent leg with a yarn 4 install
front-end, reusing the shared mock fixture) and a vendored
offline-frozen-install chain (container twin of
`e2e_vendor_yarn_berry_build.rs`: stage a manifest from the installed
bytes, `vendor --offline`, then a fresh-checkout
`yarn install --immutable --check-cache` must land the patched bytes —
proving the CLI's offline `10c0/<hex>` checksum is what real yarn 4
accepts). Both gated behind the existing `docker-e2e` feature +
`skip_if_no_docker_image`.

Assisted-by: Claude Code:claude-fable-5
…rush)

Add `e2e_redirect_rush_sim.rs`. Tier 1 (default-runnable, gated on
corepack pnpm@9): run the real CLI `scan --mode hosted` over a committed
Rush-shaped fixture (rush.json + common/config/rush/pnpm-lock.yaml),
then REPLICATE `rush install` in-test — copy the rewritten common lock
to common/temp/pnpm-lock.yaml, write a generated-style
common/temp/package.json, and run `pnpm@9 install --frozen-lockfile`
with the registry pinned to a dead port so the only reachable artifact
URL is the wiremock hosted tarball. Asserts the patched bytes land in
common/temp/node_modules, plus a tamper twin (serve wrong bytes → pnpm
integrity failure).

Tier 2 (gated on `RUSH_E2E=1`, network-dependent, off by default): real
`npm x @microsoft/rush` — `rush update` → `scan --mode hosted` →
`rush install`, asserting patched bytes, then the
preventManualShrinkwrapChanges failure + `rush update` recovery. All
`RUSH_*` env vars (including the gate) are stripped before invoking rush
since it rejects unrecognized ones.

Assisted-by: Claude Code:claude-fable-5
Silence `-D warnings` in the new redirect capstones: fold the berry
bootstrap-checksum `let...else { return None }` into the `?` operator,
and drop the unused `copy_dir_recursive` helper from the rush sim (it
rewrites locks in place and never copies a `.socket/` tree).

Assisted-by: Claude Code:claude-fable-5
… scan aliases

README: support matrix hosted npm-family cell now covers yarn-berry
(cacheKey 10c0 / compressionLevel 0, node-modules linker e2e-covered,
PnP untested for hosted) + bun (text bun.lock v1, bun.lockb auto-migrated);
adds a Rush note (hosted repoints common/config lockfiles in place incl.
subspaces, agent works through the symlink farm, vendored refused →
hosted). The maven caveats section is rewritten from the old silent-
fallback story to fail-closed version suffixing (a Socket-only
<version>-socket.<hex8> the mirror/warm-~/.m2 can't shadow, build hard-
fails on outage/tamper) plus optional Maven 3.9+ Trusted Checksums
(silently inert below 3.9; 3.9.9 MNG-8182 readability fix); ~/.m2 shadow
re-scoped to vendored-only, mirrorOf flipped to a loud failure, gradle
snippet now carries the suffixed version. Legacy scan spellings --vendor
/--apply/--sync are removed from every example and prose line; scan --mode
<hosted|vendored|agent> is the only documented selector (the booleans stay
as back-compat aliases, noted once). The standalone vendor command section
stays (home of vendor --revert). VEX (redirected) marker docs unchanged
(still accurate — nothing user-visible changed).

CHANGELOG: rewrites the stale verify-only maven-hosted entry to the fail-
closed suffixing + trusted checksums behavior; adds Unreleased entries for
berry/bun hosted (+ bun.lockb auto-migration), Rush hosted/agent/vendored
stances, the pnpm nested-lockfile generalization, and the repair redirect-
only exit-0 no-op fix.

CLI_CONTRACT: documents the redirect candidate-file set (.yarnrc.yml,
bun.lock, .mvn/*), berry/bun/rush hosted coverage and the new redirect_*
codes, the Rush lockfile-supplement discovery fallback, and repair's
redirect_only_project exit-0.

Assisted-by: Claude Code:claude-fable-5
The berry/bun rewriter unit tests built single-override slices with
`&[ovr.clone()]`, which trips clippy::cloned_ref_to_slice_refs under
the --tests lint gate. Use std::slice::from_ref instead — no clone,
same slice.

Assisted-by: Claude Code:claude-fable-5
Two CI-only failures:

- scan_redirect_migrates_bun_lockb_then_redirects is unix-only now: its
  fake bun shim is a #!/bin/sh script (Windows would need a .cmd twin
  and ';' PATH joining). The migration code path itself is OS-agnostic
  and keeps real-bun coverage in the gated e2e capstone.

- in-process vendor/revert runs acquire the apply lock with a 5s
  timeout. flock guards are OFD-based, so a concurrent test's fork()ed
  pre-exec child briefly holds copies of every parent fd — including a
  just-dropped lock fd — making back-to-back in-process runs see their
  own lock as held for the fork→exec window. Seen twice as rare
  release-only lock_held failures (revert_round_trip previously,
  revert_works_without_manifest now); the wait absorbs the window via
  the acquire loop's 100ms retry while a real deadlock still fails.

Assisted-by: Claude Code:claude-fable-5
@mikolalysenko Mikola Lysenko (mikolalysenko) merged commit e9b0a58 into main Jul 2, 2026
55 checks passed
@mikolalysenko Mikola Lysenko (mikolalysenko) deleted the feat/scan-redirect-mode branch July 2, 2026 15:23
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