Repository navigation
Stop attesting npm-family pins pnpm or a user patch overrides - #2010
Mikola Lysenko (mikolalysenko) wants to merge 4 commits into
Conversation
VEX discovery attested two kinds of npm-family pins that the hosted and vendored writers refuse, so the statement could cover bytes the install never uses. N1, pnpm gitBranchLockfile (#556): with the setting on and a pnpm-lock.<branch>.yaml present, pnpm installs the branch from that lock. Hosted withholds the root and member locks and refuses every pnpm pin; vendored refuses the project. Discovery now runs the same detection (utils::pnpm_workspace::git_branch_locks, via a new git_branch_locks_in that skips the environment, as discovery already does for the lockfile setting) and, only while the lockfile setting is on and outside Rush, marks every ref of the root and member pnpm locks UnattestedKind::PnpmGitBranchLock. N2, user-patched npm / Bun packages (#711, #367): hosted leaves a package the project patches itself unwired and never confirms it; vendored refuses. Hosted's npm gate is factored into patch::redirect::NpmUserPatches (package.json patchedDependencies keys plus non-null lock-entry `patched` records in any present npm lock), which the hosted rewriter and npm discovery now share. Bun discovery applies the rewriters' per-dependency rule (bun_lock_text::patched_dependency_key over the manifest keys, plus bun.lock's mirrored keys for the text lock) against each ref's hosted URL or vendored tarball. Matching refs are marked UnattestedKind::UserPatchedDependency. Both keep the ref (rollback / remove / list still find a pre-fix pin and the ledgers keep the wiring live, since no re-run can clear the evidence) and only vex omits it, as vex_pnpm_git_branch_lockfile / vex_npm_patched_dependency. CLI_CONTRACT.md documents both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two follow-ups under the rule "a user patch applied on top means never attest". Bun (and npm) keys naming the pinned tarball itself. #2010 kept attesting a pin whose `patchedDependencies` key names the pin's own tarball (a `bun patch` made on top of the Socket pin, #1019), because discovery reused the hosted rewriter's per-dependency rule, where such a key is no conflict. Bun applies that patch over the Socket bytes, so the install never runs the Socket patch alone. Discovery now reads the keys against no target (any key for the name@version counts) and marks such a ref UserPatchedDependency with its own detail; npm discovery reads its keys the same way. bun_user_patched_dependency_pin_is_not_attested pinned the old attested behavior and now asserts the mark. The hosted rewriter is unchanged: it keeps such a pin (and confirms it on an in-sync re-run), while vex's plan omits it through the unattested mark. Workspace members. The consent gate walked workspace members and ancestor workspace settings; discovery and the in-memory engine read the project root only. UserPatchIndex's walk now runs over a ProjectView and is shared: discover() (the gate) is the walk over the disk with every ecosystem, npm_family_in() the same walk over any view with the npm-family declarations (pnpm, yarn, patch-package), so members' package.json pnpm.patchedDependencies, yarn patch: descriptors, patches/ folders and --patch-dir, and their pnpm/yarn files are read alike. The member glob walk is rewritten over the view (same depth, skip list, limits and link handling as the walkdir one), and an ancestor with any workspace marker is walked in both scopes. The walk never reads outside the git repository root (the nearest ancestor holding .git, inclusive; in memory the project itself): the ancestor walk stops there, as before, and a workspace member or literal --patch-dir beyond it now protects every npm package with an "outside the git repository" diagnostic instead of being read, in the gate too. Without a .git the gate's home-directory rule still bounds the ancestor walk. workspace_member_patches_agree_with_the_consent_gate checks discovery and the gate agree on member cases, on disk and in memory, and on a member beyond the repository root. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Discovery never reads the environment for pnpm settings: the files-only flag now reaches `sharedWorkspaceLockfile` too (view_setting / workspace_members / new member_locks_in), and discovery's member-lock listing uses it. - A governing ancestor pnpm-workspace.yaml is read in files-only mode only from inside the project's git repository (stat-only check), so discovery never reads project files above the repository root. - User-patched marks are deferred as an UnwiredCopy for exactly that ref (new CopyTarget::Ref), so a ref a later contest drops keeps no stale reason. npm and Bun share one helper (mark_user_patched). - NpmUserPatches records keys only bun.lock mirrors, and the detail names that file instead of package.json. - A ref with no url or vendored artifact is still matched by name@version. - npm locks are walked for `patched` records only when their bytes spell `"patched"`; gitBranchLockfile detection runs only once a root or member pnpm lock wired a patch. - The branch-lock mark names the branch lock in the marked lock's own directory. - The vex_npm_patched_dependency remedy names `socket-patch rollback <uuid>`; CLI_CONTRACT.md says the tarball-key rule applies to npm and Bun alike. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A package the project patches itself is applied by the package manager on top of whatever tarball the lock names, so a Socket pin of it either fails the install or installs the user's diff stacked on the Socket patch. The CLI's consent gate (patch::user_patches::UserPatchIndex, #1646) already kept `scan` / `get` / `vendor` / `apply` from wiring such pnpm, yarn berry and patch-package packages unless --allow-patch-conflicts, but two consumers did not read that evidence: - vex discovery attested a pnpm, yarn, vlt (or npm / Bun) pin of such a package: a pin written before the patch was declared, or one an --allow-patch-conflicts run wrote. - the in-memory hosted engine (Node addon, hosted-bundle) has no gate in front of it and rewired them. One detector, three consumers. UserPatchIndex's pnpm / yarn / patch-package parse rules are factored into pure helpers (pnpm_yaml, yarn_manifest, yarn_lock, patch_package_dirs, patch_file), which the consent gate keeps using, plus UserPatchIndex::npm_family_in, which reads the same declarations through a ProjectView (disk, snapshot or memory): package.json's pnpm.patchedDependencies, yarn patch: descriptors and patch-package dirs, pnpm-workspace.yaml's patchedDependencies, the patches of the given pnpm locks (top-level map, patch_hash suffixes) and yarn.lock's user patch: locators. It leaves out npm's and Bun's own top-level patchedDependencies, which no other package manager reads and which #711 / #367 / #2010 already judge per resolution. - Discovery: a new vex::discover::user_patched pass marks every npm ref the index protects UnattestedKind::UserPatchedDependency (the ref stays, so rollback / remove / list still find it). The bytes the install runs are Socket's patch alone only when no user patch is declared, so a declared one always withholds the statement, with or without consent. - Hosted: engine::withhold_user_patched drops such candidates from every rewriter and records them skipped `user_patch_conflict` (the consent gate's code), plus a warning in memory. The in-memory engine always applies it; the CLI disk flow applies it unless --allow-patch-conflicts (it adds the Rush lock the consent gate does not read). - Vendored: unchanged. Every vendored entry point already runs the consent gate, which refuses (`vendor`, vendored writers) or skips (`scan`, `apply`) these packages. Override: the existing --allow-patch-conflicts, invocation-only. It already covered pnpm / yarn / patch-package at the consent gate and now also lifts the engine gate; vex never attests while the patch is declared. npm / Bun native patches stay refused even with consent, as before. vlt has no patch mechanism (vlt@1.6.2 has no patchedDependencies, patch_hash or `patch` command; vlt.json modifiers rewrite specs), and neither has yarn classic, so patch-package is what is read for both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two follow-ups under the rule "a user patch applied on top means never attest". Bun (and npm) keys naming the pinned tarball itself. #2010 kept attesting a pin whose `patchedDependencies` key names the pin's own tarball (a `bun patch` made on top of the Socket pin, #1019), because discovery reused the hosted rewriter's per-dependency rule, where such a key is no conflict. Bun applies that patch over the Socket bytes, so the install never runs the Socket patch alone. The shared npm / Bun helper (npm::user_patch_source, behind mark_user_patched) now reads the keys against no target (any key for the name@version counts) and flags the case where only a key for the ref's own tarball matches, which gets its own detail; npm and Bun refs alike. bun_user_patched_dependency_pin_is_not_attested pinned the old attested behavior and now asserts the mark. The hosted rewriter is unchanged: it keeps such a pin (and confirms it on an in-sync re-run), while vex's plan omits it through the unattested mark. Workspace members. The consent gate walked workspace members and ancestor workspace settings; discovery and the in-memory engine read the project root only. UserPatchIndex's walk now runs over a ProjectView and is shared: discover() (the gate) is the walk over the disk with every ecosystem, npm_family_in() the same walk over any view with the npm-family declarations (pnpm, yarn, patch-package), so members' package.json pnpm.patchedDependencies, yarn patch: descriptors, patches/ folders and --patch-dir, and their pnpm/yarn files are read alike. The member glob walk is rewritten over the view (same depth, skip list, limits and link handling as the walkdir one), and an ancestor with any workspace marker is walked in both scopes. The walk never reads outside the git repository root (the nearest ancestor holding .git, inclusive; in memory the project itself): the ancestor walk stops there, as before, and a workspace member or literal --patch-dir beyond it now protects every npm package with an "outside the git repository" diagnostic instead of being read, in the gate too. Without a .git the gate's home-directory rule still bounds the ancestor walk. workspace_member_patches_agree_with_the_consent_gate checks discovery and the gate agree on member cases, on disk and in memory, and on a member beyond the repository root. Contest survivors only. Like #2010's npm / Bun marks, the pnpm / yarn / patch-package pass now records each mark as an UnwiredCopy for exactly that ref (CopyTarget::Ref), so it lands only on a ref that survives the lock contests and a ref an earlier extractor already marked keeps that first reason; a_contested_user_patched_ref_leaves_no_mark covers it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner decision for #556 in VEX discovery: a `pnpm-lock.<branch>.yaml` at the root, or in a workspace member that installs from its own lock, withholds every root and member pnpm lock pin from attestation whatever `gitBranchLockfile` (or pnpm's `lockfile` setting) says. The check is deterministic and fail-closed, reads the project's files only (no environment, no settings, nothing above the project), and runs only once one of those locks wired a patch. New `utils::pnpm_workspace::git_branch_lock_files` lists the branch locks without reading the setting. The mark keeps the `PnpmGitBranchLock` kind and still names the branch lock in the marked lock's own directory. The detail, the vex remedy and CLI_CONTRACT.md now say a branch lock's presence withholds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A package the project patches itself is applied by the package manager on top of whatever tarball the lock names, so a Socket pin of it either fails the install or installs the user's diff stacked on the Socket patch. The CLI's consent gate (patch::user_patches::UserPatchIndex, #1646) already kept `scan` / `get` / `vendor` / `apply` from wiring such pnpm, yarn berry and patch-package packages unless --allow-patch-conflicts, but two consumers did not read that evidence: - vex discovery attested a pnpm, yarn, vlt (or npm / Bun) pin of such a package: a pin written before the patch was declared, or one an --allow-patch-conflicts run wrote. - the in-memory hosted engine (Node addon, hosted-bundle) has no gate in front of it and rewired them. One detector, three consumers. UserPatchIndex's pnpm / yarn / patch-package parse rules are factored into pure helpers (pnpm_yaml, yarn_manifest, yarn_lock, patch_package_dirs, patch_file), which the consent gate keeps using, plus UserPatchIndex::npm_family_in, which reads the same declarations through a ProjectView (disk, snapshot or memory): package.json's pnpm.patchedDependencies, yarn patch: descriptors and patch-package dirs, pnpm-workspace.yaml's patchedDependencies, the patches of the given pnpm locks (top-level map, patch_hash suffixes) and yarn.lock's user patch: locators. It leaves out npm's and Bun's own top-level patchedDependencies, which no other package manager reads and which #711 / #367 / #2010 already judge per resolution. - Discovery: a new vex::discover::user_patched pass marks every npm ref the index protects UnattestedKind::UserPatchedDependency (the ref stays, so rollback / remove / list still find it). The bytes the install runs are Socket's patch alone only when no user patch is declared, so a declared one always withholds the statement, with or without consent. - Hosted: engine::withhold_user_patched drops such candidates from every rewriter and records them skipped `user_patch_conflict` (the consent gate's code), plus a warning in memory. The in-memory engine always applies it; the CLI disk flow applies it unless --allow-patch-conflicts (it adds the Rush lock the consent gate does not read). - Vendored: unchanged. Every vendored entry point already runs the consent gate, which refuses (`vendor`, vendored writers) or skips (`scan`, `apply`) these packages. Override: the existing --allow-patch-conflicts, invocation-only. It already covered pnpm / yarn / patch-package at the consent gate and now also lifts the engine gate; vex never attests while the patch is declared. npm / Bun native patches stay refused even with consent, as before. vlt has no patch mechanism (vlt@1.6.2 has no patchedDependencies, patch_hash or `patch` command; vlt.json modifiers rewrite specs), and neither has yarn classic, so patch-package is what is read for both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two follow-ups under the rule "a user patch applied on top means never attest". Bun (and npm) keys naming the pinned tarball itself. #2010 kept attesting a pin whose `patchedDependencies` key names the pin's own tarball (a `bun patch` made on top of the Socket pin, #1019), because discovery reused the hosted rewriter's per-dependency rule, where such a key is no conflict. Bun applies that patch over the Socket bytes, so the install never runs the Socket patch alone. The shared npm / Bun helper (npm::user_patch_source, behind mark_user_patched) now reads the keys against no target (any key for the name@version counts) and flags the case where only a key for the ref's own tarball matches, which gets its own detail; npm and Bun refs alike. bun_user_patched_dependency_pin_is_not_attested pinned the old attested behavior and now asserts the mark. The hosted rewriter is unchanged: it keeps such a pin (and confirms it on an in-sync re-run), while vex's plan omits it through the unattested mark. Workspace members. The consent gate walked workspace members and ancestor workspace settings; discovery and the in-memory engine read the project root only. UserPatchIndex's walk now runs over a ProjectView and is shared: discover() (the gate) is the walk over the disk with every ecosystem, npm_family_in() the same walk over any view with the npm-family declarations (pnpm, yarn, patch-package), so members' package.json pnpm.patchedDependencies, yarn patch: descriptors, patches/ folders and --patch-dir, and their pnpm/yarn files are read alike. The member glob walk is rewritten over the view (same depth, skip list, limits and link handling as the walkdir one), and an ancestor with any workspace marker is walked in both scopes. The walk never reads outside the git repository root (the nearest ancestor holding .git, inclusive; in memory the project itself): the ancestor walk stops there, as before, and a workspace member or literal --patch-dir beyond it now protects every npm package with an "outside the git repository" diagnostic instead of being read, in the gate too. Without a .git the gate's home-directory rule still bounds the ancestor walk. workspace_member_patches_agree_with_the_consent_gate checks discovery and the gate agree on member cases, on disk and in memory, and on a member beyond the repository root. Contest survivors only. Like #2010's npm / Bun marks, the pnpm / yarn / patch-package pass now records each mark as an UnwiredCopy for exactly that ref (CopyTarget::Ref), so it lands only on a ref that survives the lock contests and a ref an earlier extractor already marked keeps that first reason; a_contested_user_patched_ref_leaves_no_mark covers it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
bugbot run Generated by Claude Code |
git_branch_lock_files ignored Members::Unresolved, so with the shared lock off and no readable member list, discovery attested the root pin even though a member's pnpm-lock.<branch>.yaml could not be ruled out. Hosted mode already treats that case as a possible branch lock; return the reason and withhold the same way (fail-closed). Addresses the Bugbot finding on pnpm_workspace.rs. Co-Authored-By: Claude <noreply@anthropic.com>
|
bugbot run Generated by Claude Code |
There was a problem hiding this comment.
✅ 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 ac92b08. Configure here.
|
Ready for review at ac92b08.
Generated by Claude Code |
Final review brief (for the human approver)What it does: VEX discovery stops attesting npm-family pins that hosted and vendored mode refuse.
Risk: low. Every change either withholds an attestation or is a refactor that keeps hosted behaviour the same. I compared the hosted Look here
Verified
Changes I made: none. Open questions (non-blocking)
Auto-merge is not armed yet, because CI is red on the main-wide pdm failure. Once #2028 lands and this goes green, the next final-reviewer run arms it, and approving then sends it straight to the merge queue. Generated by Claude Code |
Problem
VEX discovery (
vex/discover/) attested two kinds of npm-family pins that hosted and vendored mode both refuse. In both cases the install may not use the attested bytes.gitBranchLockfile(Hosted scan with pnpmgitBranchLockfilepins the stale pnpm-lock.yaml and reports success, while pnpm installs unpatched bytes from pnpm-lock.<branch>.yaml #556). When the setting is on and apnpm-lock.<branch>.yamlexists, pnpm installs the branch from that lock. Hosted leaves out the root locks and member locks and refuses every pnpm pin (hosted/engine.rs,refuse_git_branch_locks). Vendored refuses the project (vendor/pnpm_lock.rs,git_branch_lock_refusal). Discovery'sextract_pnpmnever checked the setting.patchedDependencies/ lockpatchedrecord Hosted npm scan pins a package that npm 12's nativepatchedDependenciesalso patches, so every laternpm ci/npm installfails EPATCHFAILED (and vendored refuses the lockfileVersion 4 lock with wrong advice) #711,bun patchHosted and vendored Bun rewiring silently discards the project's ownbun patch(patchedDependencies): fresh frozen installs drop the user's patch with exit 0 #367). Hosted skips these packages and records them as refused, and never confirms them. Vendored refuses them (npm v4 lock; Bun refuses the one dependency). Discovery's npm and Bun extractors never read these fields, so a pin written before Hosted npm scan pins a package that npm 12's nativepatchedDependenciesalso patches, so every laternpm ci/npm installfails EPATCHFAILED (and vendored refuses the lockfileVersion 4 lock with wrong advice) #711/Hosted and vendored Bun rewiring silently discards the project's ownbun patch(patchedDependencies): fresh frozen installs drop the user's patch with exit 0 #367 was attested.Fix
pnpm-lock.<branch>.yamlexists, at the root or in a workspace member that installs from its own lock. That holds whatevergitBranchLockfileor pnpm'slockfilesetting says, so the result is deterministic and fails closed. The check reads the project's files only: no settings, no environment, nothing above the project. Rush is exempt, and the check runs only once a root or member pnpm lock has wired a patch. A newutils::pnpm_workspace::git_branch_lock_fileslists the branch locks. When one exists, every ref of the root pnpm lock (orshrinkwrap.yaml) and of each member's own lock is markedUnattestedKind::PnpmGitBranchLock. This goes throughUnwiredCopy { scope: <lock>, target: Any }, so only refs that survive the contests get marked. Hosted and vendored refs are both marked, because vendored mode also refuses this state.patchedDependenciesalso patches, so every laternpm ci/npm installfails EPATCHFAILED (and vendored refuses the lockfileVersion 4 lock with wrong advice) #711 gate is moved out ofrewrite_npm_lockintopatch::redirect::NpmUserPatches. It holds thepackage.jsonpatchedDependencieskeys plus the non-nullpatchedrecords from any present npm lock. The hosted rewriter and npm discovery both use it now (no behaviour change for hosted). Bun discovery uses the rewriters' per-dependency rule,bun_lock_text::patched_dependency_key: forbun.lockit reads the manifest keys and the keys mirrored in the lock, the same asrewrite_bun_lock; forbun.lockbit reads the manifest keys only, the same as the hosted engine andvendor::bun_binary. Each ref is checked against its hosted URL or vendored tarball, so a key naming the pinned tarball itself is still not a conflict. Matching refs are markedUnattestedKind::UserPatchedDependency.vex_sources.rsgets note armsvex_pnpm_git_branch_lockfile/vex_npm_patched_dependency.CLI_CONTRACT.mddocuments both: the Unattested references bullet, the code table, and a cross-link from the Hosted scan with pnpmgitBranchLockfilepins the stale pnpm-lock.yaml and reports success, while pnpm installs unpatched bytes from pnpm-lock.<branch>.yaml #556 paragraph.Why keep the ref and mark it unattested, instead of a diagnostic and no ref
In both cases the writers treat the wiring as present but unchangeable, not as dead:
socket-patch rollback <uuid>for a pin written before Hosted npm scan pins a package that npm 12's nativepatchedDependenciesalso patches, so every laternpm ci/npm installfails EPATCHFAILED (and vendored refuses the lockfileVersion 4 lock with wrong advice) #711. That rollback needs the ref.scan/vendorre-run can't clear the evidence in either case.If the ref were dropped, the ledger liveness checks (
hosted_claim/vendored_claim) would report the wiring as reverted. That's the same reasoning as #2005 / #2007,BundledCopyandNpmShrinkwrapOnly.Choices worth a look
sharedWorkspaceLockfile; since 5df67ed it does not readgitBranchLockfileat all).git_branch_locks_in(view, false), the files-only reading of the setting, stays available and reads a governing ancestor's files only from inside the project's git repository. Hosted and vendored also readnpm_config_git_branch_lockfile/pnpm_config_git_branch_lockfilefrom the environment. I followed discovery's existing precedent for thelockfilesetting (lockfile_disabled(view, false): "the hosted engine runs in the install's environment,vexneed not"), sovexand the ledger checks don't depend on the scanner's shell.bun patchover an earlier hosted pin, Bun silently drops the user's patch, so the installed bytes may in fact be the pure Socket bytes. I still don't attest them: hosted refuses and never confirms, and the owner rule makes the modes the ground truth.Tests
Each new test fails on the code before this change (checked by switching the three new marking paths off: all three panic at their unattested assertions) and passes after:
vex::discover::npm::tests::pnpm_git_branch_lockfile_marks_the_lock_pins_unattested: root lock viapnpm-workspace.yamland via.npmrc, plus asharedWorkspaceLockfile: falsemember with its own branch lock. Controls: no branch lock, and the setting off.vex::discover::npm::tests::npm_user_patched_dependency_pin_is_not_attested: a manifest key, a v4 lock-entrypatchedrecord, a record in the sibling shrinkwrap only, and a key for another package or version as the control.vex::discover::bun::tests::bun_user_patched_dependency_pin_is_not_attested: hostedbun.lockwith versioned and bare keys; a key naming the pinned tarball (no conflict); a vendored ref keyed throughbun.lock's mirrored keys; hostedbun.lockb.commands::vex_sources::tests::npm_family_unattested_kinds_have_their_own_tags.pnpm_git_branch_lockfile_marks_the_lock_pins_unattestednow covers the owner decision. A branch lock still withholds when the setting istrue,false, missing, or turned off in.npmrc, and when pnpm'slockfilesetting is off. The same holds for a member's branch lock with no setting. With no branch lock (before it is written, and after it is deleted), the pin attests.Runs (after 5df67ed):
cargo test -j4 -p socket-patch-core --lib -- vex::discover:: patch::redirect:: vendor::npm vendor::bun vendor::pnpm hosted:: utils::pnpm_workspace: 2179 passed.vex::discover::testing::golden::tests::committed_fixture_corpus_matches_goldenpasses; no golden changes.cargo test -j4 -p socket-patch-cli --lib vex_sources: 15 passed.cargo test -p socket-patch-cli --test in_process_redirect_pnpm: 41 passed.cargo clippy --locked --workspace --all-features -- -D warningsandcargo fmt --all -- --checkare clean.Not done
patchedDependencies(pnpm.patchedDependencies/pnpm-workspace.yaml), yarn berrypatch:and vlt are not covered. They're out of scope for N2.Review findings
/code-review highproduced 10 findings. I confirmed all 10 and fixed them in 76bb103, a new commit on top; nothing is refuted. Where a finding is a behaviour bug, a new test failed before the fix (each panicked at its target assertion) and passes after.git_branch_locks_in(view, false)still readnpm_config_/pnpm_config_shared_workspace_lockfilethroughworkspace_members→view_setting. Discovery's existingmember_lockscall did too.view_settingandworkspace_membersnow takeenv, and discovery calls a newmember_locks_in(view, false). Discovery reads no environment variables anywhere. Test:pnpm_git_branch_lockfile_discovery_reads_no_environment(serial,EnvGuard).UserPatchedDependencymarks were pushed beforecontest_across_locks, so a dropped ref kept a stale reason. Marks are nowUnwiredCopys for exactly one ref (newCopyTarget::Ref { purl, uuid }), and they land only if that ref survives the contests. Tests:a_contested_user_patched_ref_leaves_no_mark(npm) anda_contested_user_patched_bun_ref_leaves_no_mark.NpmUserPatches::add_mirrored_keysrecords keys that onlybun.lockmirrors, and the detail now says "bun.lockpatchedDependencieshas …". Test: an added assertion inbun_user_patched_dependency_pin_is_not_attested.vex_npm_patched_dependencyremedy now namessocket-patch rollback <uuid>for a pre-Hosted npm scan pins a package that npm 12's nativepatchedDependenciesalso patches, so every laternpm ci/npm installfails EPATCHFAILED (and vendored refuses the lockfileVersion 4 lock with wrong advice) #711 pin that failsnpm ciwithEPATCHFAILED. Test: an assertion innpm_family_unattested_kinds_have_their_own_tags.patchedrecords only when its bytes contain"patched", asrewrite_npm_lockdoes.git_branch_locks_inruns only after a root lock,shrinkwrap.yamlor member pnpm lock has produced a ref.npm::mark_user_patched(withuser_patch_source). Bun's copy of the rule is gone.urlnorartifact_relis matched with an empty target, so itsname@version/ bare-name key still applies and every tarball key counts. Test:a_ref_without_a_target_is_still_matched_by_name_and_version.pnpm_git_branch_lock_mark_names_the_locks_own_branch_lock.Owner rule: no reads outside the git repository root. Discovery's files-only mode now reads a governing ancestor
pnpm-workspace.yaml/.npmrconly when that ancestor is at or below the nearest directory with a.gitentry. The check only stats paths. Outside a repository, discovery reads no ancestor at all. Hosted and vendored (env mode) are unchanged. Test:utils::pnpm_workspace::tests::the_files_only_reading_stays_inside_the_git_repository.Note for the stacked #2018: it changes the same code (the Bun tarball-key rule, a shared project walk). Its rebase will need to pick up
CopyTarget::Ref/mark_user_patchedandNpmUserPatches::add_mirrored_keys.Note
Medium Risk
Touches core VEX discovery and pnpm workspace parsing; incorrect branch-lock or user-patch detection could suppress legitimate attestations, so thorough regression testing is important.
Overview
Aligns
vexdiscovery with hosted-mode rules for pnpm git-branch locks and npm/Bun user patches so those pins are never attested while legitimate contests still remove stale reasons.pnpm git-branch locks: files-only discovery now enumerates
pnpm-lock.<branch>.yamlfiles per root/member lock without consulting env vars, tags surviving refs asPnpmGitBranchLock, and falls back to fail-closed diagnostics when member membership cannot be resolved. The CLI contract and VEX notes reference the newvex_pnpm_git_branch_lockfilecase.User patches: introduces shared
NpmUserPatchestrackingpatchedDependencies, mirrored Bun keys, and lockpatchedrecords; npm/Bun extractors mark conflicts via targetedUnwiredCopys plus new tests/docs covering remediation.Also updates
pnpm_workspacehelpers (member_locks_in,git_branch_lock_files, repo-scoped ancestor reads) and expands regression suites to pin the new behavior.Reviewed by Cursor Bugbot for commit ac92b08. Configure here.
Generated by Claude Code