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
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Jul 6, 2026
- Add "maven" to SERVICE_ECOSYSTEMS in the vendor dispatch gate. The
  maven backend has a full service path (prebuilt jar download +
  registry pom) and its own errors advise --vendor-source=service,
  which the gate dead-ended with vendor_service_unsupported_ecosystem.
  Unblocks the PR #117 regression guard service_mode_gate_admits_maven
  failing on all platforms. Also syncs the stale ecosystem list in the
  comment and refusal message (both omitted nuget and maven).

- vex/product.rs: use strip_prefix instead of manual starts_with +
  slice (clippy::manual_strip, -D warnings).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Jul 7, 2026
…t, docs sync (−587 net lines) (#120)

* refactor(cli): behavior-preserving simplify pass across the command layer

Dedup and privatize across all 14 command files + args.rs: shared
patch_matches/short_uuid/error_envelope/DownloadParams/offline-diagnostic
helpers, merged duplicate emit blocks, deleted write-only structs and dead
fields, pub -> pub(crate) for the internal fetch_stage/lock_cli/repair_vendor
libraries. Core: are_safe_redirect_coords widened to pub so the CLI VEX
go-patches synthesis applies the exact core guard instead of a local mirror
(test retargeted). Net -575 lines; no user-visible behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(cli): unify env-bool parsing — one parser policy, scrub covers local vars

parse_bool_flag everywhere: get --save-only/--one-off and remove
--skip-rollback had clap's default bool-from-env (SOCKET_X=1 or an
exported-but-empty var aborted the parse — same bug class fixed on
rollback/repair/unlock/vex earlier); apply --check, setup --check/--remove,
and get/scan --all-releases dropped BoolishValueParser (rejects VAR=).
SOCKET_STRICT was missing from GLOBAL_ARG_ENV_VARS despite the list's
'every env var GlobalArgs binds' contract. New LOCAL_ARG_ENV_VARS mirrors
that contract for subcommand flags; the main-run scrub and the test
harness's clean-env helper now cover both lists. Two table-driven
invariant tests parse every binding against its owning subcommand with
'', '1', 'yes', 'garbage' so the policy self-enforces (RED-verified
against the pre-fix get.rs).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(remove): implement --dry-run — preview, no mutations

remove was the one mutating command that silently ignored the global
--dry-run (contract row: 'Preview, no mutations') and deleted anyway.
Now: lock/emit carry dry_run; the confirmation prompt is skipped (nothing
to confirm); rollback and vendor-revert run their existing dry-run modes;
the manifest removal is simulated in memory so the blob sweep previews
against the post-removal reference set; the envelope reports dryRun:true
with would-be Removed events flipped to Verified previews (the
apply/vendor/repair convention) so summary.removed stays 'entries
actually deleted'. Telemetry does not fire on previews. Contract lines
103/763 updated. +2 invariant tests (RED-verified against the old code).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(get): --one-off fails honestly instead of silently saving anyway

The flag parsed but was never read past the --save-only conflict check —
the patch was saved to the manifest regardless, lying about persistence.
It now errors 'One-off get mode is not yet implemented' up front (before
any network/disk activity), mirroring rollback --one-off's contract.
Contract env-table row documents both stubs; the in_process_get caveat
test now pins the honest error instead of the accidental behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(scan): invert the mode fold — ScanMode is the source of truth

resolve_mode_flags previously folded --mode INTO the legacy booleans
(--redirect/--vendor/--apply), leaving the deprecated spellings as the
internal truth. Now the booleans fold into args.mode and are never read
past the fold; run() derives apply/vendor/hosted locals from the enum.
Error messages are byte-identical (pinned by scan_vendor_e2e). Legacy
boolean help text now says 'Deprecated spelling of --mode X'. Contract
tests updated to pin the new direction (modeless scan stays read-only,
--sync == --mode agent --prune).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(core): warm-up dedups — normalize_file_path, serialize_sorted, stale vendor docs

rollback.rs drops its private normalize_file_path copy for apply's pub
one; serialize_sorted (manifest/schema.rs + vendor/state.rs identical
copies) moves to utils/serde.rs; the vendor/mod.rs + npm_flavor.rs doc
comments claiming 'only the package-lock backend exists' are rewritten
to match reality (all five npm flavors + five pypi flavors + maven/nuget
backends dispatch).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(core): consolidate 7 atomic_write copies into utils::fs::atomic_write_bytes

pth_hook/edit, package_json/update, composer_setup, gem_setup/update,
go_redirect, patch/apply (write_atomic), and manifest/operations
(write_manifest's inline block) were byte-identical reimplementations of
the stage+fsync+rename+dir-fsync pattern; each becomes a 2-line
delegating wrapper keeping its per-file 'why this must not tear' doc
(the go_mod_edit precedent). blob_fetcher's write_cache_entry_atomic is
deliberately exempt (no fsync — re-downloadable content-addressed cache)
and now says so, so the next dedup pass doesn't flag it. ~−230 lines.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(vendor): hoist shared leaf helpers into vendor/common.rs

already_patched_verify (x11), synthesized_result (x10 + golang's
success-shaped variant), refused (x6), and copy_matches_after_hashes
(x4 + go_redirect's inline loop) were per-backend copies of the same
bodies; they move to patch/vendor/common.rs and every backend imports
them. Deliberate tightening: cargo/golang's copy_hashes_ok copies
lacked the is_safe_relative_subpath guard the composer/gem versions
carry — the shared helper is the guarded one, so their in-sync probes
now fail closed (re-vendor) on an escaping manifest key instead of
hashing through it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(vendor): ungate go_h1_of_zip — minimal builds failed to compile

verify_go_h1 (ungated, called from the ecosystem-agnostic service-download
path whenever the service reports a dirhashH1) called the golang-gated
go_h1_of_zip, so --no-default-features didn't build. Ungating the hasher
(all its deps are unconditional) keeps dirhash verification fail-closed in
every build; gating the caller instead would have silently skipped content
verification in non-golang builds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(crawlers): delegate coordinate guards to path_safety, dedup is_dir

The six per-crawler is_safe_* guards re-derived the same
reject-..-/absolute/separator/NUL predicate but were one predicate
weaker than patch/path_safety: none rejected ':', so a Windows
drive-relative coordinate (C:evil) could join as an escape. Each guard
body now delegates to is_safe_single_segment/is_safe_multi_segment
(strictly stricter, fail-closed; colon-rejection cases added RED-first
to every guard's tests). The identical async is_dir copied across the
crawlers moves to utils::fs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(vendor): restore ApplyResult import in composer_lock test mod

The vendor/common.rs hoist trimmed prod imports; composer_lock's
test-mod unwrap_done helper (composer-gated, so default builds never
compiled it) lost its ApplyResult path. Caught by the crawler-guard
pass's --all-features test build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(vendor-cli): collapse the 8-way dispatch into a local macro

Every backend takes the identical 9-argument tuple; a local macro_rules!
keeps the per-arm #[cfg] while removing ~90 lines of repetition.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(scan): extract gc into scan/gc.rs (move-only)

Convert commands/scan.rs to the directory module commands/scan/ and move
the GC block (GcSummary, run_gc, run_apply_gc, preview_apply_gc, gc_json,
print_gc_vendored_line, detect_prunable) plus its unit tests verbatim into
scan/gc.rs. Only use statements, path qualifiers (super::vendor ->
crate::commands::vendor), and visibility (pub(super)) changed; zero
behavior change. Gate suite (cli_parse_scan, scan_invariants,
in_process_scan, scan_sync_e2e, scan_vendor_e2e, in_process_redirect,
cli_scan_silent, output_modes_e2e) green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(scan): extract discovery into scan/discovery.rs (move-only)

Move UpdateInfo, LockfileSupplement, lockfile_supplement, crawled_from_purl,
vendored_ledger_supplement, preverify_vendor_baselines, detect_updates,
collect_vuln_ids, and severity_order plus their unit tests verbatim into
scan/discovery.rs. Only use statements and visibility (pub(super)) changed;
the shared manifest_with test helper stays in mod.rs (also used by gc
tests). Zero behavior change; scan gate suite green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(scan): extract hosted into scan/hosted.rs (move-only)

Move the self-contained hosted/redirect flow (REDIRECT_CANDIDATE_FILES,
parse_purl_simple, run_redirect) verbatim into scan/hosted.rs. Only use
statements and visibility (pub(super) run_redirect, pub(super)
discover_selected in mod.rs) changed; zero behavior change. Scan gate
suite green (incl. in_process_redirect).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(scan): extract vendor_flow into scan/vendor_flow.rs (move-only)

Move the vendor-mode flow (preview_vendor_json, run_scan_vendor_step,
run_vendor_json_path, run_vendor_interactive_path,
partition_skipped_selected, fold_vendored_skips_into_apply, all boxed_*
transient-frame wrappers kept verbatim, track_outcomes_for_vendor) into
scan/vendor_flow.rs. Only use statements and visibility (pub(super))
changed; the Windows-stack boxed_* wrappers are untouched. Zero behavior
change; scan gate suite green (incl. scan_vendor_e2e).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(scan): add module docs to scan/mod.rs, tidy blank line (move-only cleanup)

Final pass of the scan.rs -> scan/ split: module-level docs describing the
submodule layout and one leftover double blank line. Zero behavior change;
full gate green (scan gate suite, --lib 303 tests, clippy --all-targets
zero warnings, interactive_prompts_e2e).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: sync CLI_CONTRACT + README with reality — unlock documented, tables completed

Contract: unlock row + env/exit-code/envelope-enum/per-command-args
table completions, --strict correctly global, --redirect marked
hidden/deprecated (v4 removal), empty-manifest apply exit-0 reconciled,
migration-status notes for unlock/vendor/vex. README: unlock command
section + an "Undoing things" ownership table for the six recovery
verbs. Stale "currently RED" comment in cli_dry_run_paths_e2e removed
(the pinned behavior is green).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: fix four more README/contract inaccuracies found during the sync

README: --manifest-path has no -m short form; --break-lock never
deletes the lock file (reclaim, not force-remove); --json status
vocabulary split camelCase-envelope vs snake_case-legacy documented
(apply's jq example was showing legacy statuses it never emits);
--strict added to the global-options table. Contract: SOCKET_VEX row
now names vendor --vex alongside apply/scan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: address all five confirmed findings from the adversarial review

1 (medium): remove/rollback --dry-run no longer mutate .socket/blobs —
the preview previously create_dir_all'd the blobs dir and downloaded
missing before-hash blobs into it (files a wet run's sweep would have
deleted). Blob reads now stage in a throwaway tempdir_in(.socket)
sibling: cached blobs hardlink in, downloads land there, auto-removed
on drop. Offline/present semantics unchanged; +1 RED-verified test.
2 (low): remove's not-found/error envelopes now thread dry_run, so
preview failures report dryRun:true (matching apply and remove's own
success envelope).
3 (low, pre-existing): verify_go_redirect_state's drift loop now
applies is_safe_relative_subpath before hashing — a poisoned manifest
key like ../../../../../secret can no longer leak an arbitrary file's
existence + content hash into apply --check output; fails closed as
StaleCopy{found:None}. +1 RED-verified security test.
4 (low): rollback's --one-off bail hoisted above API-client
construction so the contract's 'before any network or disk activity'
claim is true (client construction can resolve the org slug remotely).
5 (low): dropped needless pub(super) on three scan/mod.rs helpers —
child modules reach parent-private items; restores pre-split
encapsulation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cleanup pass, wip

* chore: remove accidentally committed .DS_Store files

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* wip clean up

* .

* refactor: remove all ecosystem feature flags — every ecosystem always compiled in

The cargo/golang/maven/composer/nuget/deno Cargo features are gone from
both crates; npm, PyPI, Ruby gems, Go, Cargo, NuGet, Maven, Composer, and
Deno support is now unconditional. All #[cfg(feature)] gates, not(feature)
fallback stubs (inert Go apply/rollback stubs, apply --check fail-closed
exit-2 stub, composer setup no-ops), and stale feature-set comments are
deleted. The only remaining features are the CLI's docker-e2e / setup-e2e
test-suite gates. Runtime env gates (SOCKET_EXPERIMENTAL_MAVEN / _NUGET)
are unchanged.

CI drops the explicit ecosystem feature lists; Dockerfile.base builds
default; README / CLI_CONTRACT / CHANGELOG updated (flagged MAJOR for
anyone scripting --features).

Verified: clippy -D warnings clean across --all-targets --all-features;
full workspace suite green (3,370 tests / 148 binaries, 0 failures).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* clean up pass complete

* follow up bug fix sweep

* bug fix sweep

* commit changes so far

* fix CI: admit maven at the vendor service gate; clippy manual_strip

- Add "maven" to SERVICE_ECOSYSTEMS in the vendor dispatch gate. The
  maven backend has a full service path (prebuilt jar download +
  registry pom) and its own errors advise --vendor-source=service,
  which the gate dead-ended with vendor_service_unsupported_ecosystem.
  Unblocks the PR #117 regression guard service_mode_gate_admits_maven
  failing on all platforms. Also syncs the stale ecosystem list in the
  comment and refusal message (both omitted nuget and maven).

- vex/product.rs: use strip_prefix instead of manual starts_with +
  slice (clippy::manual_strip, -D warnings).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: 10 prod bugs + 7 test-harness bugs surfaced by a no-fail-fast full-suite sweep

CI has been failing one binary per run (cargo fail-fast hides every later
binary), so a local `cargo test --workspace --all-features --no-fail-fast`
sweep (155 targets) was run to flush the whole backlog at once. Prod fixes:

- scan/hosted.rs: --silent swallowed the redirect-VEX failure ("errors
  only", never "nothing" — exit 1 with no message is undiagnosable), and
  human mode never printed the rewriter's own warnings (JSON already
  carried them; e.g. the no-lockfile warning).
- lock_cli.rs: lock_held/lock_io errors were muted under --silent; the
  error and its remediation hint now always print in human mode.
- get.rs: the selection_required error advised `--id <UUID>`, a
  value-taking form the CLI rejects (--id is a boolean type-tag); it now
  directs re-running with the chosen UUID as the identifier.
- repair_vendor.rs: rebuilds from an installed copy for RECONSTRUCTED
  entries (ledger gone) were never verified against the rewired lockfile's
  integrity — a drifted/tampered copy was blessed into a fresh ledger.
  Register the wired integrity for those candidates (same trust anchor as
  the unverified-registry rung), and snapshot/restore the wiring files so
  a rejected rebuild can't leave the backend's integrity refresh behind.
- cargo_config.rs: wrote into .cargo/config.toml when a legacy
  .cargo/config coexists — cargo reads the extensionless file and warns,
  so the [patch] entry was silently inert; and the atomic write reset the
  destination's permission bits (now mode-preserving, matching the other
  lock writers).
- cargo.rs: re-vendoring a new patch uuid over live wiring failed on the
  already-detached lock entry and unwound the live config entry (bricking
  builds); a detach failure mid-re-vendor dropped the prior socket entry
  instead of restoring it; and an artifact-only rebuild of a wholesale-
  deleted uuid dir never restored the committed marker file.
- pypi.rs/pypi_requirements.rs: the requirements flavor had no in-sync or
  stale-uuid pre-flight (unlike uv/poetry/pdm/pipenv) — every re-run
  appended a duplicate wheel line and clobbered the ledger record. Same
  uuid is now the in-sync/artifact-rebuild path; a different uuid refuses
  (pypi_requirements_already_vendored) before any writes.
- pth_hook/detect.rs + setup.rs: the lockfile refresh ran bare
  `poetry lock` / `pdm lock`, re-resolving the user's whole pinned set on
  Poetry 1.x; the refresh now tries the pin-preserving spelling first
  (`lock --no-update` / `lock --update-reuse`) with bare `lock` as the
  fallback for versions that dropped the flag.

Test-harness fixes (hermeticity/scrub bugs that fail suites on their own):

- cli_parse_vendor/e2e_safety_unlock/remove_rollback_api_overrides: env
  scrub lists were missing SOCKET_VENDOR_SOURCE / SOCKET_VENDOR_URL /
  SOCKET_PATCH_SERVER_URL / SOCKET_STRICT (drift guards demanded them);
  remove_rollback also gains the standard drift-guard test.
- setup_matrix_deno.rs: the roundtrip plants SOCKET_STRICT decoys its own
  fixed-list scrub didn't strip; converted to the prefix scrub its pypi
  sibling uses (with the telemetry force-off).
- e2e_golang.rs: hostile env seeds were added without the paired
  env_remove lines, so SOCKET_ECOSYSTEMS=npm leaked and filtered the go
  crawler out of every scan.
- e2e_vendor_yarn_classic_build.rs: corepack() scrubbed env AFTER applying
  caller overrides, wiping its own YARN_CACHE_FOLDER (the fixture install
  silently used the global cache); scrub-then-seed like the berry sibling.
- scan_vendor_e2e.rs: run_cli_env never scrubbed or disabled telemetry, so
  runs phoned the configured endpoint (and the live API in ambient-env
  shells); prefix scrub + SOCKET_TELEMETRY_DISABLED=1, caller env last.

Verified: full workspace sweep + targeted reruns of every affected binary
(all green), core lib 1895 tests, cli lib 318 tests, clippy -D warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(windows CI): docker_vendor_common_selftest ran against the WSL bash stub

has_bash() only checked that `bash` SPAWNED — on windows-latest,
Command::new("bash") resolves to the System32 WSL stub (no distro
installed), which spawns fine but cannot run a script: it prints its
error as UTF-16 on stdout and exits non-zero, failing all three
stage_patch selftests with empty stderr. Probe an actual `bash -c` run
and require the sentinel back, so hosts without a functional bash skip
(the snippets' real target is the Linux docker images; unix CI hosts
keep full coverage).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(windows): maven package_path mixed OS separator into the reported rel

jar_path was built as leaf_dir.join(&jar_leaf), which inserts an OS
separator (`\` on Windows) before the leaf while the rest of the
reported path keeps the rel's forward slashes — package_path is
reported (and asserted) as a display string, so
wired_rebuild_reports_vendored_jar_path failed on windows-latest.
Join the already-present full forward-slash rel (jar_copy_rel) off
project_root instead; same file, consistent report on every platform.

First-ever windows execution of this test: cargo runs test binaries as
they finish compiling, and every earlier windows run died in some other
binary first.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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