Skip to content

Fix silent hosted pnpm pin beside a file: copy - #1469

Merged
Mikola Lysenko (mikolalysenko) merged 4 commits into
mainfrom
agent/fix-pnpm-file-copy-scan-veto
Oct 13, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 4 commits into
mainfrom
agent/fix-pnpm-file-copy-scan-veto

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

LLM Description written by Claude Code:claude-opus-5-5

Refs #935

This fixes the hosted half of #935. Vendored pnpm still writes its
override silently beside a file: copy. That gap is in the pnpm vendor
backend, which has no post-write discovery check to hook into, so it stays
open on #935 as a follow-up (see below).

What users saw

Take a pnpm workspace where one member depends on registry left-pad@1.3.0
and another on a file: directory or tarball copy of the same release.
scan --mode hosted pinned the registry entry and reported success
with no warning about the file: copy, which pnpm keeps installing
unpatched. The same run's --vex then attested the package not_affected.
A later standalone vex correctly refused (patched_ref_unattributable).
Bisected by the pnpm bug-hunt to f3c6313 (#1008).

Root cause

The hosted engine re-runs lockfile discovery over the rewrite before
writing (unattributed_pins in hosted/engine.rs). Before #1008, a pin
whose own lock also installs an unpatched copy of the same name@version
was dropped from discovery, so the engine skipped the patch as
redirect_unattributable. #1008 (for #828) moved such refs into
Discovery::shadowed so rollback and remove can still undo them, and
counted them as pins, so the engine let the pin through silently.

The pnpm rewriter can't see these copies itself, because a directory
copy's version lives in its own package.json, not in the lock. So nothing
recorded the uuid in bundled_skipped_uuids, and the in-run VEX assumed it
applied.

Fix

The registry entry is still pinned, which matches npm
(redirect_npm_non_registry_entry_skipped) and yarn classic
(redirect_yarn_classic_directory_skipped). The registry copy is what most
of the app loads, so skipping the whole patch would leave it vulnerable
too. For each shadowed hosted pin whose own pnpm lock carries a file:
copy (from discovery's unpatched_copies), the engine now:

  • adds a redirect_copy_unpatched warning naming the lock and the entry,
    once per (lock, entry);
  • records the uuid in bundled_skipped_uuids, so the in-run --vex does
    not attest it (the same mechanism the yarn classic rewriter uses).

CLI_CONTRACT.md documents the new warning code and updates the
attribution-gate paragraph.

Review notes

/code-review high ran twice.

  • The first pass rejected the original approach of vetoing the whole patch,
    because it contradicts the npm and yarn classic policy of patching the
    registry copy and warning about the other one. It also changed npm
    behaviour without a test. Reworked in d14593d.
  • The second pass found that the in-run VEX still attested the package,
    that dedup by matching warning text was fragile, and that the warning
    wasn't scoped to pnpm. All three are fixed in this push: the uuid joins
    bundled_skipped_uuids, the warning is limited to pnpm locks and keyed
    by (file, entry), and there's a new --vex test.
  • Not changed: a reviewer suggested moving detection into the pnpm
    rewriter. The rewriter only sees lock text, and a directory copy's
    version needs its package.json. Discovery already reads that
    (record_pnpm_file_copies), so the engine reuses it rather than
    duplicating the read.

Tests

New in crates/socket-patch-cli/tests/in_process_redirect_pnpm.rs:

Case Test Without fix With fix
#935 directory copy, scan warns hosted_pnpm_file_directory_copy_is_named fails (no warning) passes
#935 tarball copy, scan warns hosted_pnpm_file_tarball_copy_is_named fails (no warning) passes
#935 in-run --vex doesn't attest hosted_pnpm_file_copy_is_not_attested_by_the_in_run_vex fails (not_affected statement) passes
  • in_process_redirect_pnpm: 35/35 pass.
  • cargo fmt --check and cargo clippy --workspace --all-features -D warnings: clean.
  • cargo test --workspace --all-features: every target passes except 13 tests that fail because of this container (not checked against main here). 12 are permission-based write-failure tests, which can't fail a write when the container runs as root. One is an e2e_bun_lockb test whose rollback fails reaching patches-api.socket.dev (tunnel error). The same 13 failed before and after each change in this PR, and none of them is in the hosted pnpm or engine-gate path. CI is the check for them.

Follow-up (stays on #935)

Vendored pnpm: scan --mode vendored still writes
overrides: {left-pad@1.3.0: file:.socket/vendor/…} beside a file: copy
without a warning. npm and Bun vendored emit
vendor_non_registry_entry_skipped here. The pnpm backend
(vendor/pnpm_lock.rs preflight) needs the same, reading the copy's
version the way record_pnpm_file_copies does.

🤖 Generated with Claude Code

https://claude.ai/code/session_01H7rWeixkY6z9hT2QNYmWLC


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
Since #1008, a hosted scan of a pnpm workspace where one member uses
the registry release and another a file: directory or tarball copy of
the same name@version pinned the registry entry and said nothing.
pnpm keeps installing the file: copy unpatched, and vex refuses to
attest the pin, so the scan wrote wiring nothing could vouch for.

#1008 moved such refs into Discovery::shadowed so rollback can still
undo them, and the scan's veto counted them as attributed pins. The
veto now treats a pin seen only as shadowed like contested wiring:
the patch is skipped as redirect_unattributable, naming the file:
entry, and the lock is left as found. Bundled, alias, vlt bundled
and takeover pins keep their exemptions.

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko Mikola Lysenko (mikolalysenko) changed the title Fix silent hosted pin beside pnpm file: copy (#935) Fix silent hosted pnpm pin beside a file: copy Oct 13, 2026
Reviewing the first fix showed that vetoing the whole patch was the
wrong call. npm and yarn classic already pin the registry copy, which
most of the app loads, and warn about the copy they can't reach.
Skipping the patch on pnpm would leave the registry copy vulnerable
too.

So the veto is gone. A hosted scan now pins the registry entry as
before and adds a redirect_copy_unpatched warning that names each
same-lock unpatched copy (a pnpm file: directory or tarball dep)
beside the pin. The warning is left out when the rewriter already
reported that copy. CLI_CONTRACT.md lists the new code.

Assisted-by: Claude Code:claude-opus-5-5
The same hosted run that pinned a pnpm registry entry beside a
file: copy also attested the package not_affected in its own --vex,
although that copy installs unpatched. The engine now records such
pins' uuids with the skipped bundled copies, so the in-run VEX
verifies them instead of assuming them applied.

The redirect_copy_unpatched warning is limited to pnpm locks, where
the rewriter can't see these copies, and is given once per lock and
entry instead of being matched against other warnings' text.

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 13, 2026 08:04
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

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

Reviewed by Cursor Bugbot for commit 33b60e7. Configure here.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 13, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Labeled Ready for review by the burn-down agent.

  • Head: 33b60e7f00 (mergeable against main, no conflicts; CHANGELOG.md untouched)
  • CI: 36/36 green + 50 path-skipped; no main-wide failures
  • Bugbot: reviewed 33b60e7, no new issues; no open review threads
  • Reviewer focus: the hosted pnpm path in hosted/engine.rs that now refuses/warns instead of silently pinning beside a same-lock file: copy, and in_process_redirect_pnpm.rs. The vendored half of Warn during pnpm scans when non-registry copies cannot be patched #935 is a follow-up.
  • Slack announcement not sent: the Slack send tool isn't available to this run.

Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Final review brief

What it does. In hosted scan/get, the engine now detects when a pin it just confirmed is shadowed by a pnpm file: directory or tarball copy of the same name@version in the same pnpm-lock.yaml/shrinkwrap.yaml. For each such pin it emits a redirect_copy_unpatched warning and adds the uuid to bundled_skipped_uuids, so the in-run --vex no longer attests the package not_affected. The registry pin is still written, as for npm and yarn classic.

Risk: low. It only adds a warning and makes the in-run VEX more conservative. Which pins are written, the veto logic and the exit codes don't change. The extra work is a filter over a discovery the engine already runs, with no new I/O.

Look here

  • engine.rs:1892-1927: the detection loop. It uses the same purl + file predicate as Discovery::contest_within_locks and is scoped to pnpm lock filenames.
  • engine.rs:1734-1735: the results are merged only on the final, non-vetoed pass.
  • scan/hosted.rs:1757-1762:`` the existing consumer that removes skipped uuids from assume_applied.
  • in_process_redirect_pnpm.rs:2650-2825:``` the 3 new tests.

Verified

  • Read the full diff and the surrounding engine and discovery code.
  • cargo test -p socket-patch-cli --test in_process_redirect_pnpm: 35/35 pass. cargo test -p socket-patch-core --lib hosted::engine: 60/60 pass.
  • With the merge lines at engine.rs:1734-1735 reverted, all 3 new tests fail, so they exercise the fix.
  • CI is 36/36 green on this head, including ci-ok and clippy. Bugbot found no issues. CHANGELOG.md is untouched and there are no open threads.

Changes I made: none.

Open questions (non-blocking nits)

  • hosted_pnpm_file_copy_is_not_attested_by_the_in_run_vex turns a missing VEX file into an empty list. It does fail without the fix today, but asserting that the file exists would harden it.
  • The VEX skip works per uuid, so the same patch pinned in a second, clean lock also falls back to installed-tree verification. That is conservative, and the CLI_CONTRACT.md row could say so.

Auto-merge is armed: approving sends this straight to the merge queue.


Generated by Claude Code

Merged via the queue into main with commit 9c523e4 Oct 13, 2026
87 of 118 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the agent/fix-pnpm-file-copy-scan-veto branch October 13, 2026 14:15
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 13, 2026
Covers #1469, #1470, #1474, #1475, #1478, #1479, #1480 and #1482; #1484 is
CI-only. The notes now cover main through 285a5e8.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
github-merge-queue Bot pushed a commit that referenced this pull request Oct 13, 2026
* ci(release): publish npm from release.yml, not publish-npm.yml

Every socket-patch npm package's trusted publisher is registered against
release.yml, and npm allows only one publisher per package. The 5.0.0
release dispatched publish-npm.yml, whose OIDC token named that file, so
npm rejected it with E401 before staging anything.

Move the npm leg back inline as release.yml's npm-publish job (same steps
as publish-npm.yml) and delete publish-npm.yml, so the existing
registration keeps working. Add an `npm-retry-version` input that skips
the version gate, build, tag, GitHub release and crates.io jobs and
re-stages npm from an existing release's assets; this retries 5.0.0 now
and replaces the manual Publish npm dispatch. crates.io stays dispatched
through publish-cargo.yml, since crates.io allows several publishers per
crate.

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

* chore(release): fold 22 more PRs into the 5.0.0 notes

Cover the PRs merged to main after #1432's base (1779317..4cb3643):
Cargo source replacement and registry = "crates-io" (#338/#455, #386),
pnpm lockfile=false (#1074), berry removed descriptors (#1435), yarnrc
${VAR} reads (#1100), CRLF .npmrc append (#1196), Composer vendor-dir
cascade and global home (#439/#686, #586), Go +incompatible vex (#484),
gem moved BUNDLE_GEMFILE and commented Gemfile revert (#952, #988),
Poetry source = "pypi" (#1282) and Hatch path characters (#547).

No entries for #1411 (macOS fix to code #1393 added in this release;
the #1393 entry already describes the intended behavior), #1426 (perf
regression introduced and fixed within 5.0.0) or the CI/test/bench PRs.

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

* chore(release): cover #1419, #1434, #1440, #1442, #1444 in the 5.0.0 notes

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

* chore(release): 5.0.1

Cut [Unreleased] into [5.0.1] — 2026-10-13 and stamp 5.0.1 with
scripts/version-sync.sh. The 5.0.1 notes cover main through 501c6d9
(#1449), i.e. every PR after the v5.0.0 tag (6ad6add): the npm
publish fix (5.0.0 never reached npm or crates.io, 5.0.1 is the first
v5 there), #1465, #759, #1460, #1462, #1445, #869, #1447, #1450,
#1467, and #1449 under Maintenance. CI/test-only PRs (#1448, #1452,
#1454, #1457, #1459) have no entries.

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

* chore(release): 5.0.1 notes for PRs merged since the cut

Covers #1469, #1470, #1474, #1475, #1478, #1479, #1480 and #1482; #1484 is
CI-only. The notes now cover main through 285a5e8.

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants