Skip to content

fix: harden allowBuilds artifact approvals#12294

Merged
zkochan merged 19 commits into
mainfrom
security/cand-pnpm-123-allowbuilds-deppath
Jun 10, 2026
Merged

fix: harden allowBuilds artifact approvals#12294
zkochan merged 19 commits into
mainfrom
security/cand-pnpm-123-allowbuilds-deppath

Conversation

@zkochan

@zkochan zkochan commented Jun 9, 2026

Copy link
Copy Markdown
Member

Summary

Package-name allowBuilds entries no longer approve lifecycle scripts for artifacts whose identity a name cannot pin: git, git-hosted tarball, direct tarball, and local directory dependencies. To approve such an artifact explicitly, use its peer-suffix-free lockfile depPath as the allowBuilds key — error hints, pnpm ignored-builds, and pnpm approve-builds print exactly that key.

  • AllowBuild policy functions identify packages by DepPath instead of caller-supplied name/version. The policy parses name and version out of the depPath itself, so name-keyed rules can never be fed an identity that disagrees with the gated artifact. AllowBuildContext carries only an explicit trustPackageIdentity override, used to evaluate a previously recorded policy under its original semantics when detecting revoked approvals.
  • Identity trust is derived from the depPath shape: a registry-style depPath (name@semver) is a trusted identity. This is sound because lockfile verification runs a new unconditional, offline structural pass that rejects lockfiles where such a key is backed by a git, directory, or git-hosted tarball resolution (ERR_PNPM_RESOLUTION_SHAPE_MISMATCH), while the npm resolution verifier already binds explicit tarball URLs of semver-keyed entries to the registry's own dist.tarball unconditionally. The pass runs inside the existing candidate walk and participates in the verification cache key (resolutionShapeCheck) on both the gate's and the fresh-resolve record paths, so the stat-only cache fast path stays sound and records written before the rule existed are re-verified.
  • Installs detect approvals that were revoked (or stopped applying) for git/tarball artifacts and surface those packages as ignored builds; approvals granted for previously ignored builds trigger a rebuild of exactly those packages.
  • preparePackage always treats the fetched manifest as an untrusted identity: it requires a pkgResolutionId and gates on the synthesized name@<resolution id> depPath. scp-style git URLs are normalized to ssh:// form in resolution ids, and the git fetcher reuses createGitHostedPkgId from the resolver instead of re-deriving ids.
  • Under the global virtual store, pnpm rebuild locates a projection created before the approval was granted by following the project's node_modules link, since the projection hash includes the allowBuilds verdict (relocating the projection instead is tracked in rebuild: relocate global virtual store projections instead of building in place #12302).
  • New shared helpers: removePeersSuffix() in @pnpm/deps.path (string-level peer-suffix stripping for user-written keys) and allowBuildKeyFromIgnoredBuild() in @pnpm/building.policy (the key under which an ignored build is approved).
  • pacquet mirrors the whole policy: AllowBuildPolicy::check(dep_path) derives trust from the dep path, the git-fetcher allow-build closures take only the dep path, pacquet-lockfile-verification gains the same structural pass, error code, and cache identity, and dep-path keys normalize via remove_suffix.
  • shell-quote is overridden to 1.8.4 (GHSA-w7jw-789q-3m8p / CVE-2026-9277).
  • Test-harness fix: a module-level drain keeps the global log stream flowing in the deps-installer lifecycle tests, so reporter assertions no longer receive the buffered backlog of earlier installs that ran without a reporter.

Validation

  • TypeScript suites: building.policy, deps.graph-hasher, exec.prepare-package, fetching.git-fetcher, fetching.tarball-fetcher, building.commands (policy + rebuild, including a new regression test for rebuilding in the global virtual store after a post-install approval), installing.deps-installer (lifecycle scripts, lockfile-verification gate + cache), pnpm dlx e2e.
  • Rust suites: cargo nextest for pacquet-git-fetcher, pacquet-package-manager, pacquet-lockfile-verification; clippy with -D warnings, rustfmt, dylint, and taplo via the pre-push hook.
  • Full CI green: CI, Pacquet CI, Pacquet Code Coverage, CodeQL, Audit, zizmor, and both pacquet benchmark workflows.

Written by agents (Codex, GPT-5 and Claude Code, claude-fable-5).

Summary by CodeRabbit

  • New Features

    • Build approvals now require trusted package identity for git and direct-tarball artifacts; allowlist entries can target exact lockfile depPath identities.
    • The allow-build context now exposes depPath, resolution, and a trust flag; Global Virtual Store lookups prefer existing hashed dirs but can fall back to linked symlink resolution.
  • Bug Fixes

    • Prevents unintended lifecycle script execution for untrusted/git/tarball identities.
    • More reliable rebuild/install behavior and GVS layout resolution for source-hosted deps.

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR migrates allowBuild to accept a depPath plus an AllowBuildContext (trustPackageIdentity), adds dep-path identity keys and normalization, computes trust from resolution/resolvedVia, propagates metadata through resolvers/graphs, and updates call sites, fetchers, installers, GVS, Rust policy, and tests to use the new contract.

Changes

Build Policy and Package Identity Trust

Layer / File(s) Summary
Policy core and tests
building/policy/src/index.ts, building/policy/test/index.ts
Add AllowBuildContext, createAllowBuildContext, allowBuildKeyFromIgnoredBuild, dep-path vs package-spec rule partitioning, and trust heuristics; update tests to cover trust and invalid keys.
Core types
core/types/src/config.ts
Change AllowBuild signature to (depPath, context?) and add AllowBuildContext type.
Ignored-build key handling
building/commands/src/policy/*, installing/commands/src/handleIgnoredBuilds.ts
Derive ignored-build keys via allowBuildKeyFromIgnoredBuild instead of parsing names; adjust approve/getAutomaticallyIgnoredBuilds and ignored-build listings.
After-install selectors & GVS
building/after-install/src/index.ts, building/commands/test/build/index.ts
Support { depPath } selectors, match selectors by normalized depPath, call allowBuild with createAllowBuildContext, and add GVS linked-dir fallback resolution.
During-install build gating
building/during-install/src/index.ts
Call allowBuild with node.depPath and createAllowBuildContext(node) instead of name/version.
Resolver & graph metadata
installing/deps-resolver/src/*, deps/graph-builder/src/lockfileToDepGraph.ts, deps/graph-hasher/src/index.ts
Yield/propagate resolution and resolvedVia metadata and call allowBuild with constructed context when computing built dep paths.
Installer/restore/link call sites
installing/deps-installer/src/*, installing/deps-restorer/src/*, installing/deps-installer/src/install/link.ts
Pass depPath + createAllowBuildContext to allowBuild in link, hoisted linking, unignored build runs, and revoked-approval detection; use getPkgIdWithPatchHash for rebuild identifiers.
Prepare/package and fetchers
exec/prepare-package/src/*, fetching/git-fetcher/src/*, fetching/tarball-fetcher/src/*, resolving/git-resolver/src/*
Add pkgResolutionId/dep_path, compute git/tarball pkgResolutionId, and call preparePackage/allowBuild with synthesized depPath and trust=false for artifacts.
Tests & e2e updates
installing/deps-installer/test/*, building/commands/test/*, fetching/*/test/*, exec/commands/test/dlx.e2e.ts
Update tests to derive dynamic depPath keys, add tarball lifecycle test, and assert trust-dependent allow/deny behavior.
Rust pacquet policy & callers
pacquet/crates/package-manager/src/*, pacquet/crates/git-fetcher/*
AllowBuildPolicy supports dep-path keys and check_with_context; prepare_package and fetchers updated to new callback shape; virtual-store/GVS callers compute trust and depPath when gating builds.
Workspace & manifests
.changeset/*, pnpm-workspace.yaml, multiple package.json and tsconfig.json
Add @pnpm/building.policy workspace dependency to many packages, add TS references, pin shell-quote, and add release note.

Estimated code review effort: 🎯 4 (Complex) | ⏱️ ~60 minutes

"🐰 I hop through locks and depPaths bright,
Trusting identities before I bite.
Builds now ask kindly, do you vouch?
I nibble scripts only with a trusted pouch.
Hooray for safe builds and a carrot lunch!"

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: harden allowBuilds artifact approvals' accurately describes the main change: requiring trusted package identity before package-name allowBuilds entries can approve lifecycle builds for artifacts.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch security/cand-pnpm-123-allowbuilds-deppath

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@zkochan zkochan force-pushed the security/cand-pnpm-123-allowbuilds-deppath branch from 0fb2c4e to 8939ad5 Compare June 9, 2026 19:14
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.03      5.0±0.10ms   874.3 KB/sec    1.00      4.8±0.27ms   898.4 KB/sec

@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Each scenario reports direct installs and pnpr installs. Bencher consumes pacquet@HEAD and pnpr@HEAD.

Scenario: Isolated linker: fresh restore, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 10.314 ± 0.168 10.208 10.654 1.90 ± 0.06
pacquet@main 10.277 ± 0.126 10.205 10.606 1.89 ± 0.05
pnpr@HEAD 5.436 ± 0.136 5.299 5.758 1.00
pnpr@main 5.450 ± 0.158 5.302 5.820 1.00 ± 0.04
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 10.31357480088,
      "stddev": 0.1682821483406344,
      "median": 10.23536635238,
      "user": 3.255627439999999,
      "system": 3.3214429599999997,
      "min": 10.20779217388,
      "max": 10.65390506088,
      "times": [
        10.598718656880001,
        10.313594632880001,
        10.65390506088,
        10.20937575588,
        10.21593648088,
        10.22898596188,
        10.24174674288,
        10.20779217388,
        10.21273596388,
        10.252956578880001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 10.276961247280001,
      "stddev": 0.1255636240863856,
      "median": 10.21254091488,
      "user": 3.2719158399999997,
      "system": 3.31710066,
      "min": 10.20487420788,
      "max": 10.60613774188,
      "times": [
        10.21062738788,
        10.27138096088,
        10.21036235288,
        10.21039086588,
        10.21286456388,
        10.21221726588,
        10.20487420788,
        10.27183542188,
        10.60613774188,
        10.35892170388
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 5.43597003628,
      "stddev": 0.13574802248289128,
      "median": 5.40986779138,
      "user": 2.49019294,
      "system": 2.9913655599999993,
      "min": 5.29913213688,
      "max": 5.75816215588,
      "times": [
        5.37770098288,
        5.44203459988,
        5.33875208488,
        5.37454833588,
        5.48784779488,
        5.31154139388,
        5.44209782688,
        5.52788305088,
        5.29913213688,
        5.75816215588
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 5.4499742827799995,
      "stddev": 0.15773626957662712,
      "median": 5.377866234880001,
      "user": 2.4857402399999993,
      "system": 3.0042431599999997,
      "min": 5.30191117988,
      "max": 5.82012109488,
      "times": [
        5.30191117988,
        5.37123741788,
        5.330141465880001,
        5.47835161688,
        5.38205911188,
        5.3736733578800004,
        5.82012109488,
        5.33878913988,
        5.54379234288,
        5.55966609988
      ]
    }
  ]
}

Scenario: Isolated linker: fresh restore, hot cache + hot store

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 648.1 ± 7.9 636.9 662.8 1.00
pacquet@main 665.1 ± 10.0 647.0 678.7 1.03 ± 0.02
pnpr@HEAD 779.2 ± 65.2 741.2 957.6 1.20 ± 0.10
pnpr@main 785.1 ± 69.4 744.6 973.4 1.21 ± 0.11
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.6480816608200001,
      "stddev": 0.007916934885130303,
      "median": 0.6471735929200001,
      "user": 0.37598235999999996,
      "system": 1.32931402,
      "min": 0.6368856799200001,
      "max": 0.6627796439200001,
      "times": [
        0.6457016579200001,
        0.6627796439200001,
        0.6368856799200001,
        0.6427420459200001,
        0.6493084489200001,
        0.6462606699200001,
        0.65579180692,
        0.6480865159200001,
        0.6388820859200001,
        0.6543780529200001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.6650810359200001,
      "stddev": 0.010045972844961996,
      "median": 0.6672847979200001,
      "user": 0.37191856000000006,
      "system": 1.34038902,
      "min": 0.64695436392,
      "max": 0.6786790409200001,
      "times": [
        0.6667850089200001,
        0.6680522659200001,
        0.6621381959200001,
        0.6585109789200001,
        0.6762344939200001,
        0.64695436392,
        0.6531027089200001,
        0.6677845869200001,
        0.6786790409200001,
        0.6725687149200001
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.7792431392200001,
      "stddev": 0.06522956560344192,
      "median": 0.7540621029200001,
      "user": 0.38839816000000005,
      "system": 1.3575941200000001,
      "min": 0.7411615079200001,
      "max": 0.9575985179200001,
      "times": [
        0.7411615079200001,
        0.7754480689200001,
        0.7804139149200001,
        0.7461503069200001,
        0.7466692999200001,
        0.9575985179200001,
        0.7604410019200001,
        0.7476832039200001,
        0.74315460192,
        0.7937109679200001
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.78508450682,
      "stddev": 0.06940547257029529,
      "median": 0.7609526254200001,
      "user": 0.39146316,
      "system": 1.34215762,
      "min": 0.7446227699200001,
      "max": 0.9734397349200001,
      "times": [
        0.7993152159200001,
        0.8023462519200001,
        0.7580201059200001,
        0.7457537299200001,
        0.7512441919200001,
        0.9734397349200001,
        0.7657426379200001,
        0.7638851449200001,
        0.7464752849200001,
        0.7446227699200001
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 9.224 ± 0.046 9.189 9.334 1.81 ± 0.02
pacquet@main 9.220 ± 0.051 9.149 9.275 1.81 ± 0.02
pnpr@HEAD 5.089 ± 0.051 5.017 5.171 1.00
pnpr@main 5.099 ± 0.030 5.036 5.138 1.00 ± 0.01
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 9.22393501734,
      "stddev": 0.0458108553288942,
      "median": 9.20318118374,
      "user": 3.5934363399999993,
      "system": 3.3175235,
      "min": 9.18927345474,
      "max": 9.33371170174,
      "times": [
        9.19148250074,
        9.18927345474,
        9.23828126574,
        9.261857517740001,
        9.19582126874,
        9.20954988374,
        9.190244056740001,
        9.33371170174,
        9.19681248374,
        9.23231603974
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 9.219522806839999,
      "stddev": 0.05091091824698436,
      "median": 9.22538705524,
      "user": 3.6060664399999993,
      "system": 3.3045235000000006,
      "min": 9.14918589374,
      "max": 9.27540290974,
      "times": [
        9.212282997740001,
        9.14918589374,
        9.15504852174,
        9.27540290974,
        9.20636569674,
        9.272507924740001,
        9.27251065674,
        9.25452840474,
        9.23849111274,
        9.15890394974
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 5.08883403524,
      "stddev": 0.05131198736158377,
      "median": 5.09166804024,
      "user": 2.3107602399999996,
      "system": 2.8620053000000008,
      "min": 5.01651607574,
      "max": 5.17120557374,
      "times": [
        5.06088809774,
        5.14445034274,
        5.11388571174,
        5.17120557374,
        5.02512035974,
        5.01651607574,
        5.04750117474,
        5.0969554747400005,
        5.0863806057400005,
        5.12543693574
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 5.09916515864,
      "stddev": 0.030277911297965053,
      "median": 5.104242188740001,
      "user": 2.30650094,
      "system": 2.8935749999999993,
      "min": 5.03580012774,
      "max": 5.13772838474,
      "times": [
        5.11615517074,
        5.09865910374,
        5.07945044574,
        5.13578906474,
        5.03580012774,
        5.08821899074,
        5.11020585074,
        5.10982527374,
        5.13772838474,
        5.07981917374
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, hot cache + hot store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.410 ± 0.015 1.393 1.441 2.08 ± 0.07
pacquet@main 1.403 ± 0.014 1.386 1.424 2.07 ± 0.07
pnpr@HEAD 0.678 ± 0.023 0.657 0.737 1.00
pnpr@main 0.687 ± 0.077 0.648 0.905 1.01 ± 0.12
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.40951243594,
      "stddev": 0.014820437435387027,
      "median": 1.40648435614,
      "user": 1.5562699999999998,
      "system": 1.78996954,
      "min": 1.3933117376400002,
      "max": 1.4409042166400001,
      "times": [
        1.40668715664,
        1.42817358864,
        1.40037268164,
        1.4062815556400001,
        1.40884831064,
        1.41320557964,
        1.4025103326400001,
        1.3948291996400002,
        1.4409042166400001,
        1.3933117376400002
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.4032797258400003,
      "stddev": 0.013577131089162479,
      "median": 1.3986116061400002,
      "user": 1.5534367999999998,
      "system": 1.7843861399999998,
      "min": 1.38636109764,
      "max": 1.4239260816400001,
      "times": [
        1.3983274166400002,
        1.42213601464,
        1.38883714864,
        1.39616212964,
        1.38636109764,
        1.39889579564,
        1.40661467964,
        1.3942690346400002,
        1.4172678596400001,
        1.4239260816400001
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6784664314400002,
      "stddev": 0.022611249853267915,
      "median": 0.67186877864,
      "user": 0.33375799999999994,
      "system": 1.3039025400000002,
      "min": 0.6573152656400001,
      "max": 0.7371397246400001,
      "times": [
        0.7371397246400001,
        0.6794470126400001,
        0.6873469146400001,
        0.6818857616400001,
        0.6689483646400001,
        0.6720590736400001,
        0.6693080546400001,
        0.6573152656400001,
        0.6595356586400001,
        0.67167848364
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.6871697412400001,
      "stddev": 0.07721976369280653,
      "median": 0.6662378306400001,
      "user": 0.3356513,
      "system": 1.2686546399999998,
      "min": 0.6478949586400001,
      "max": 0.9049919946400001,
      "times": [
        0.67079162564,
        0.6478949586400001,
        0.6522299576400001,
        0.9049919946400001,
        0.6517495126400001,
        0.6593550256400001,
        0.6781958806400001,
        0.6700964066400001,
        0.67401279564,
        0.6623792546400001
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, cold cache + hot store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.995 ± 0.025 4.962 5.050 7.18 ± 0.21
pacquet@main 4.970 ± 0.030 4.918 5.011 7.15 ± 0.21
pnpr@HEAD 0.695 ± 0.020 0.668 0.725 1.00
pnpr@main 0.702 ± 0.071 0.656 0.899 1.01 ± 0.11
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.9945924132599995,
      "stddev": 0.024689653253851945,
      "median": 4.99144822156,
      "user": 1.75535426,
      "system": 1.9159239999999997,
      "min": 4.96193104256,
      "max": 5.05022347956,
      "times": [
        4.99214712456,
        4.98067214656,
        4.99464706856,
        5.01145079056,
        4.98343591056,
        4.99074931856,
        5.00857036756,
        4.97209688356,
        4.96193104256,
        5.05022347956
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.97048391076,
      "stddev": 0.029841372023626283,
      "median": 4.97447128956,
      "user": 1.76122646,
      "system": 1.9030434999999997,
      "min": 4.91788343656,
      "max": 5.01109332756,
      "times": [
        4.98366090356,
        4.97273139556,
        4.97076533056,
        4.91788343656,
        4.97621118356,
        4.99057130556,
        4.99435327156,
        5.01109332756,
        4.92155726156,
        4.96601169156
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.69538089906,
      "stddev": 0.020419719982585044,
      "median": 0.6889438855600001,
      "user": 0.34533756,
      "system": 1.3071393,
      "min": 0.66849981956,
      "max": 0.72543791256,
      "times": [
        0.7220474475599999,
        0.67909352556,
        0.71242725056,
        0.67905665056,
        0.68398389756,
        0.72543791256,
        0.66849981956,
        0.69390387356,
        0.6795107875599999,
        0.70984782556
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.70161629576,
      "stddev": 0.0711325176406548,
      "median": 0.67733528706,
      "user": 0.34346156,
      "system": 1.2755782,
      "min": 0.65555372956,
      "max": 0.89908418456,
      "times": [
        0.70406431356,
        0.68298184356,
        0.89908418456,
        0.66896825556,
        0.67232886956,
        0.67640508656,
        0.65555372956,
        0.67826548756,
        0.67109153856,
        0.70741964856
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12294
Testbedpacquet
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
milliseconds (ms)
(Result Δ%)
Upper Boundary
milliseconds (ms)
(Limit %)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
🚷 view threshold
9,223.94 ms
(+17.19%)Baseline: 7,870.61 ms
9,444.73 ms
(97.66%)
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
🚷 view threshold
4,994.59 ms
(-0.55%)Baseline: 5,022.02 ms
6,026.43 ms
(82.88%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,409.51 ms
(-0.58%)Baseline: 1,417.71 ms
1,701.25 ms
(82.85%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
10,313.57 ms
(+6.71%)Baseline: 9,665.28 ms
11,598.33 ms
(88.92%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
648.08 ms
(-1.47%)Baseline: 657.75 ms
789.30 ms
(82.11%)
🐰 View full continuous benchmarking report in Bencher

@zkochan zkochan force-pushed the security/cand-pnpm-123-allowbuilds-deppath branch from 8939ad5 to 17d1bfc Compare June 9, 2026 20:03
@zkochan zkochan marked this pull request as ready for review June 9, 2026 20:10
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (13) 📘 Rule violations (0) 📎 Requirement gaps (2)

Context used

Grey Divider


Action required

1. runUnignoredDependencyBuilds rebuilds in-place 📎 Requirement gap ≡ Correctness ⭐ New
Description
The in-install dependency build flow (runUnignoredDependencyBuildsbuildSelectedPkgs) triggers
rebuilds without any relocation/import to a newly computed built-hash slot. This means the same
non-relocating GVS rebuild behavior applies during install-time rebuilds, violating the requirement
that both standalone rebuild and in-install builds follow the relocation design.
Code

installing/deps-installer/src/install/index.ts[R1094-1111]

async function runUnignoredDependencyBuilds (
  opts: StrictInstallOptions,
  previousIgnoredBuilds: IgnoredBuilds,
+  currentLockfile: LockfileObject,
  allowBuild?: AllowBuild
): Promise<Set<DepPath>> {
  if (!allowBuild) {
    return previousIgnoredBuilds
  }
  const pkgsToBuild: string[] = []
  for (const ignoredPkg of previousIgnoredBuilds) {
-    const parsed = dp.parse(ignoredPkg)
-    if (!parsed.name || !parsed.version) continue
-    const allowed = allowBuild(parsed.name, parsed.version)
-    if (allowed === true) {
+    if (currentLockfile.packages?.[ignoredPkg] == null) continue
+    if (allowBuild(ignoredPkg) === true) {
      // Package is explicitly allowed - rebuild it
-      pkgsToBuild.push(`${parsed.name}@${parsed.version}`)
+      pkgsToBuild.push(dp.getPkgIdWithPatchHash(ignoredPkg))
    }
  }
  if (pkgsToBuild.length) {
Evidence
Rule 4 requires relocation behavior to apply to both standalone rebuild and the in-install
dependency build flow. The changed in-install path explicitly calls buildSelectedPkgs(...), which
runs through building/after-install's GVS rebuild directory selection and build execution without
any relocation step, so install-time rebuilds are subject to the same non-relocating behavior.

Relocation behavior applies to both standalone rebuild and in-install dependency build flow
installing/deps-installer/src/install/index.ts[1094-1117]
building/after-install/src/index.ts[363-385]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The in-install rebuild path (`runUnignoredDependencyBuilds`) delegates to `buildSelectedPkgs`, which uses the same GVS rebuild machinery that currently does not relocate/import projections into the newly computed built-hash slot.

## Issue Context
Compliance requires relocation-based behavior to be applied consistently in both standalone `pnpm rebuild` and the in-install dependency build flow.

## Fix Focus Areas
- installing/deps-installer/src/install/index.ts[1094-1117]
- building/after-install/src/index.ts[291-420]
- building/after-install/src/index.ts[363-385]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Git-host tarball case bypass 🐞 Bug ⛨ Security
Description
The new resolution-shape gate calls isGitHostedTarballUrl(tarball) to reject git-hosted tarballs
under registry-style depPaths, but isGitHostedTarballUrl only matches lowercase host prefixes. A
tampered lockfile can uppercase the host (e.g. https://CODELOAD.GITHUB.COM/...) and set gitHosted:
false to bypass ERR_PNPM_RESOLUTION_SHAPE_MISMATCH, re-enabling name-based allowBuilds approvals for
git-hosted artifacts.
Code

installing/deps-installer/src/install/verifyLockfileResolutions.ts[R291-294]

+  if (typeof tarball === 'string' && tarball !== '') {
+    if (!/^https?:\/\//i.test(tarball)) return false
+    if (isGitHostedTarballUrl(tarball)) return false
+  }
Evidence
The shape gate relies on isGitHostedTarballUrl(tarball) for rejection, but the helper’s
implementation uses case-sensitive startsWith(...) checks on lowercase host prefixes; therefore
changing the host casing prevents detection. Pacquet mirrors the same pattern
(starts_with("https://codeload.github.com/")) and consumes it in its own registry-shape predicate,
so the bypass applies there as well.

installing/deps-installer/src/install/verifyLockfileResolutions.ts[258-295]
lockfile/utils/src/toLockfileResolution.ts[74-83]
pacquet/crates/lockfile-verification/src/verify_lockfile_resolutions.rs[254-272]
pacquet/crates/lockfile/src/resolution.rs[382-392]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The resolution-shape check rejects git-hosted tarballs by calling `isGitHostedTarballUrl(tarball)`, but that helper uses case-sensitive `startsWith('https://codeload.github.com/')`-style checks. An attacker can bypass git-hosted detection by changing the URL’s host casing (hostnames are case-insensitive), causing a git-hosted tarball to be treated as registry-shaped and thus eligible for “trusted identity” name-based allowBuilds approvals.
## Issue Context
- The TS verifier uses `isGitHostedTarballUrl(tarball)` as part of the “registry-shaped resolution” predicate.
- `isGitHostedTarballUrl` currently matches only lowercase prefixes.
- Pacquet mirrors the same logic via `is_git_hosted_tarball_url`.
## Fix Focus Areas
- installing/deps-installer/src/install/verifyLockfileResolutions.ts[258-295]
- lockfile/utils/src/toLockfileResolution.ts[74-83]
- pacquet/crates/lockfile/src/resolution.rs[382-392]
- pacquet/crates/lockfile-verification/src/verify_lockfile_resolutions.rs[261-272]
## Implementation notes
- Make git-hosted tarball detection robust to host casing by parsing the URL and comparing `hostname` lowercased (and optionally normalizing the whole URL for the specific host-prefix check).
- Prefer URL parsing over `toLowerCase()` on the full string if you want to avoid altering case-sensitive path segments; only the hostname needs normalization.
- Apply the same hardening on the pacquet side to keep behavior consistent.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. GVS rebuild builds in place 📎 Requirement gap ≡ Correctness
Description
When the recomputed built-hash slot is missing, pnpm rebuild falls back to an existing projection
directory—potentially discovered by following node_modules symlinks into the global virtual
store—and runs build scripts there, mutating a not-built-hash projection and leaving built artifacts
under the wrong slot. This violates the requirement to relocate/import into the newly computed
built-hash slot before building and to avoid heuristic-based directory selection.
Code

building/after-install/src/index.ts[R376-379]

+      const preferredGvsDir = path.join(globalVirtualStoreDir, hash)
+      gvsDirByDepPath.set(pkgMeta.depPath, fs.existsSync(preferredGvsDir)
+        ? preferredGvsDir
+        : findLinkedGvsDir(pkgMeta.name, Object.values(ctx.projects), globalVirtualStoreDir) ?? preferredGvsDir)
Evidence
The compliance rules require rebuild to execute build scripts from the newly computed built-hash
slot and not mutate an existing not-built-hash projection; however, the code path selects a
preferredGvsDir and then explicitly falls back to another directory when that slot doesn’t exist,
enabling in-place builds in the old projection. Additionally, the PR introduces findLinkedGvsDir()
and uses it as a fallback, meaning rebuild can choose its working directory via symlink traversal
heuristics rather than the recomputed built-hash slot. The added test further corroborates the
problem by computing pkgInGvs from the pre-existing hash directory created during install without
approvals (i.e., the not-built hash) and then asserting that a postinstall-generated file exists in
that same directory after rebuild, demonstrating that built outputs can remain under the
not-built-hash slot post-rebuild.

Rebuild relocates global virtual store projections to the newly computed built-hash slot
building/after-install/src/index.ts[376-379]
building/after-install/src/index.ts[553-567]
building/commands/test/build/index.ts[549-587]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`pnpm rebuild` can execute build scripts in-place within an existing global virtual store projection (including one located via `node_modules` symlink traversal) when the newly recomputed built-hash slot is missing, which mutates a not-built-hash projection and can leave built artifacts stored under the not-built-hash directory.
## Issue Context
Compliance requires rebuild to first relocate/import the package into `<globalVirtualStoreDir>/<builtHash>/node_modules/<name>` (the newly computed built-hash slot) and then run build scripts from that relocated directory, so the on-disk layout accurately reflects built state (engine/deps partitioning). Rebuild must not select its build directory via heuristics such as following `node_modules` symlinks into the global virtual store; instead, the rebuild location must be determined by the recomputed built-hash slot.
## Fix Focus Areas
- building/after-install/src/index.ts[363-475]
- building/after-install/src/index.ts[376-379]
- building/after-install/src/index.ts[553-575]
- building/commands/test/build/index.ts[544-587]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (10)
4. Tarball scheme trust bypass 🐞 Bug ⛨ Security
Description
The new isRegistryShapedResolution() returns true for tarball resolutions regardless of URL scheme,
so a tampered lockfile can back a registry-style depPath (name@semver) with a non-http(s) tarball
(e.g. file:) and still be treated as trusted identity for name-based allowBuilds. The npm verifier
explicitly skips non-http(s) tarballs, so it will not provide the “tarball URL binding” safety net
in this case.
Code

installing/deps-installer/src/install/verifyLockfileResolutions.ts[R258-287]

+function isRegistryShapedResolution (resolution: unknown): boolean {
+  if (resolution == null) return true
+  if (typeof resolution !== 'object') return false
+  const { type, gitHosted, tarball, variants } = resolution as {
+    type?: unknown
+    gitHosted?: unknown
+    tarball?: unknown
+    variants?: unknown
+  }
+  if (type === 'variations') {
+    return Array.isArray(variants) && variants.every(
+      (variant) => isRegistryShapedResolution((variant as { resolution?: unknown })?.resolution)
+    )
+  }
+  // Custom resolver protocols (`type: 'custom:*'`) are a legitimate
+  // non-registry source the user opted into. They can only be materialized by
+  // a project-configured custom fetcher — an unrecognized custom type throws at
+  // fetch time (see @pnpm/fetching.pick-fetcher) — so a forged custom type
+  // cannot launder an artifact past this gate into a build.
+  if (typeof type === 'string' && type.startsWith('custom:')) return true
+  if (type != null) return false
+  // Plain tarball / registry resolution. The lockfile is parsed from YAML
+  // without schema validation, so the `gitHosted` flag is not trustworthy on
+  // its own: a tampered entry could set a non-boolean (dodging a strict
+  // `=== true`) or an explicit `false` on a git-host URL (the loader only
+  // backfills the flag when absent). Treat any non-boolean flag as git-hosted
+  // and fall back to the URL so the verdict never depends on the flag alone.
+  if (gitHosted != null && (typeof gitHosted !== 'boolean' || gitHosted)) return false
+  if (typeof tarball === 'string' && isGitHostedTarballUrl(tarball)) return false
+  return true
Evidence
The shape gate currently only rejects git-hosted tarballs, so non-http(s) tarballs (e.g. file:) pass
as “registry-shaped”. Separately, the npm verifier classifies non-http(s) tarballs as
non-npm-registry resolutions and skips verification, meaning the tarball URL binding won’t run for
those entries.

installing/deps-installer/src/install/verifyLockfileResolutions.ts[258-287]
resolving/npm-resolver/src/createNpmResolutionVerifier.ts[911-927]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`isRegistryShapedResolution()` currently treats any plain tarball resolution as registry-shaped unless it is git-hosted. This allows a semver-shaped depPath (`name@semver`) to be paired with a non-registry tarball URL (notably `file:` or other non-http(s) schemes) without triggering `ERR_PNPM_RESOLUTION_SHAPE_MISMATCH`.
Because the npm resolution verifier deliberately skips non-http(s) tarballs, this creates a trust-gap: the depPath is considered a trusted identity for `allowBuilds` name rules, but the artifact is not registry-backed.
### Issue Context
- Shape gate: `isRegistryShapedResolution()` only checks `gitHosted` and `isGitHostedTarballUrl(tarball)`; it does not check tarball URL protocol.
- Npm verifier: skips non-http(s) tarballs, so it won't bind them to registry `dist.tarball`.
### Fix Focus Areas
- installing/deps-installer/src/install/verifyLockfileResolutions.ts[258-287]
### Suggested change
In `isRegistryShapedResolution()`:
- If `tarball` is a string, require it to be an `http:` or `https:` URL to be considered registry-shaped.
- Treat unparseable URLs (or any non-http(s) scheme, including `file:`) as **non-registry-shaped** for semver depPaths.
This keeps the “trusted identity” invariant aligned with what the npm verifier can actually bind.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Pacquet tarball scheme bypass 🐞 Bug ⛨ Security
Description
pacquet’s is_registry_shaped_resolution() treats any non–git-hosted tarball as registry-shaped
without validating the tarball URL scheme, so a semver dep key can be trusted while pointing at a
non-http(s) artifact. The pacquet npm verifier skips non-http(s) tarballs, so it won’t enforce the
tarball URL binding for these entries.
Code

pacquet/crates/lockfile-verification/src/verify_lockfile_resolutions.rs[R254-266]

+/// Mirrors upstream's `isRegistryShapedResolution`: a plain tarball
+/// resolution is registry-shaped because the npm verifier unconditionally
+/// binds explicit tarball URLs of semver-keyed entries to the registry's
+/// own `dist.tarball`. A git-hosted tarball is not — and trust is gated on
+/// the tarball URL rather than the `gitHosted` flag alone, so a tampered
+/// entry that clears the flag (`gitHosted: false`) on a git-host URL is
+/// still rejected.
+fn is_registry_shaped_resolution(resolution: &LockfileResolution) -> bool {
+    match resolution {
+        LockfileResolution::Registry(_) => true,
+        LockfileResolution::Tarball(tarball) => {
+            tarball.git_hosted != Some(true) && !is_git_hosted_tarball_url(&tarball.tarball)
+        }
Evidence
The structural gate currently accepts tarball resolutions without checking scheme, while the pacquet
npm verifier explicitly declines to treat non-http(s) tarballs as npm-registry resolutions (so it
won’t bind/verify them). Together, that permits a semver key to be trusted while pointing at a
non-registry tarball.

pacquet/crates/lockfile-verification/src/verify_lockfile_resolutions.rs[254-275]
pacquet/crates/resolving-npm-resolver/src/create_npm_resolution_verifier.rs[835-855]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`is_registry_shaped_resolution()` currently returns `true` for any tarball resolution that is not git-hosted by flag/host-prefix. This includes `file:` (and other non-http(s)) tarballs.
However, the pacquet npm verifier intentionally skips non-http(s) tarballs, so those entries don’t get the unconditional tarball URL binding. That leaves a gap where registry-style keys can still be treated as trusted while backing a non-registry artifact.
### Issue Context
- Structural pass: `is_registry_shaped_resolution()` does not validate tarball URL scheme.
- Verifier: `npm_registry_tarball()` returns `None` for non-http(s) schemes, so binding/policy checks don't run.
### Fix Focus Areas
- pacquet/crates/lockfile-verification/src/verify_lockfile_resolutions.rs[254-275]
### Suggested change
In `is_registry_shaped_resolution()` for `LockfileResolution::Tarball`:
- Parse `tarball.tarball` as a URL and require scheme `http` or `https` (and still reject git-hosted URLs).
- If parsing fails or scheme is not http(s), return `false` (not registry-shaped) for semver-keyed entries.
This aligns “registry-shaped” with what the verifier can actually validate/bind.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. SCP regex breaks ssh URLs 🐞 Bug ≡ Correctness
Description
normalizeGitRepoForPkgResolutionId() can match valid URLs like ssh://user@host:2222/path (because
it contains user@host:) and rewrites them into an invalid ssh://ssh://... form, corrupting the
pkgResolutionId used for git dependency identity and allowBuild gating.
Code

resolving/git-resolver/src/createGitHostedPkgId.ts[R13-15]

+function normalizeGitRepoForPkgResolutionId (repo: string): string {
+  const scp = /^([^@\s]+@[^:\s]+):(.+)$/.exec(repo)
+  return scp == null ? repo : `ssh://${scp[1]}/${scp[2]}`
Evidence
The normalization helper unconditionally applies an SCP regex to repo, and the repo strings
accepted by the git resolver include git+ssh://... forms with a port-like colon segment; those
inputs contain ://, @, and :, so they can be mistakenly rewritten as SCP syntax, yielding
malformed IDs.

resolving/git-resolver/src/createGitHostedPkgId.ts[3-16]
resolving/git-resolver/test/index.ts[452-476]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`normalizeGitRepoForPkgResolutionId()` uses an SCP-style regex that also matches fully-qualified ssh URLs with ports (e.g. `ssh://git@host:2222/org/repo.git`). This produces malformed pkgResolutionIds like `ssh://ssh://git@host/2222/org/repo.git`, which then break identity matching (allowBuild depPath keys) and can break fetch/prepare for git deps.
### Issue Context
The regex should only apply to true SCP-like inputs (`user@host:path`) that *do not* already have a URL scheme. Today it can also match `ssh://...`/`git+ssh://...` strings that include `@` and a `:` for the port.
### Fix Focus Areas
- resolving/git-resolver/src/createGitHostedPkgId.ts[3-16]
### Suggested fix
- In `normalizeGitRepoForPkgResolutionId`, first short-circuit when `repo` already contains a scheme (`repo.includes('://')`) or starts with `git+`.
- Alternatively, tighten the regex with a negative lookahead for `://` (e.g. `^(?!.*://)([^@\s]+@[^:\s]+):(.+)$`).
- Add a unit test for `createGitHostedPkgId({ repo: 'ssh://git@github.com:2222/org/repo.git', ... })` ensuring it is left unchanged (aside from the existing `git+` prefixing behavior).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. gitHosted type bypass 🐞 Bug ⛨ Security
Description
The new resolution-shape gate treats a semver depPath as “registry-shaped” unless
resolution.gitHosted === true, so a tampered lockfile can set gitHosted to a non-boolean (e.g.
'true') to bypass ERR_PNPM_RESOLUTION_SHAPE_MISMATCH. Because lockfiles are parsed via js-yaml
without schema validation, this can re-enable package-name allowBuilds approvals for git-hosted
tarballs hidden behind name@semver keys.
Code

installing/deps-installer/src/install/verifyLockfileResolutions.ts[R248-259]

+function isRegistryShapedResolution (resolution: unknown): boolean {
+  if (resolution == null) return true
+  if (typeof resolution !== 'object') return false
+  const { type, gitHosted, variants } = resolution as { type?: unknown, gitHosted?: unknown, variants?: unknown }
+  if (type === 'variations') {
+    return Array.isArray(variants) && variants.every(
+      (variant) => isRegistryShapedResolution((variant as { resolution?: unknown })?.resolution)
+    )
+  }
+  if (type != null) return false
+  if (gitHosted === true) return false
+  return true
Evidence
The shape predicate only rejects boolean true for gitHosted, while the lockfile is loaded from
YAML without runtime typing, and the loader explicitly avoids overwriting any non-null gitHosted
value—so a string value can persist and evade the new gate.

installing/deps-installer/src/install/verifyLockfileResolutions.ts[248-259]
lockfile/fs/src/read.ts[135-141]
lockfile/fs/src/lockfileFormatConverters.ts[176-185]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`isRegistryShapedResolution()` only rejects git-hosted tarballs when `gitHosted === true`. Since lockfiles are loaded from YAML into unvalidated JS objects, a malicious lockfile can set `gitHosted: 'true'` (string) to bypass the new `RESOLUTION_SHAPE_MISMATCH` invariant and make a git-hosted tarball appear registry-trusted.
### Issue Context
- Lockfile YAML is parsed with `js-yaml` into plain JS objects (no runtime type validation).
- The lockfile loader’s `enrichGitHostedFlag()` won’t correct invalid `gitHosted` values because it early-returns on any non-null value.
### Fix Focus Areas
- installing/deps-installer/src/install/verifyLockfileResolutions.ts[248-259]
- lockfile/fs/src/lockfileFormatConverters.ts[176-185]
### Recommended fix
1. In `isRegistryShapedResolution()`, treat any non-boolean `gitHosted` as non-registry-shaped (reject), e.g.:
- If `'gitHosted' in resolution` and `gitHosted != null && typeof gitHosted !== 'boolean'` then return `false`.
- Keep rejecting when `gitHosted === true`.
2. Optionally harden `enrichGitHostedFlag()` to only treat boolean `gitHosted` as set; if it’s non-boolean, either normalize (if you can safely) or leave it unset so URL-based enrichment can set a correct boolean.
3. Add a unit test showing a semver depPath with a codeload tarball resolution and `gitHosted: 'true'` is rejected with `ERR_PNPM_RESOLUTION_SHAPE_MISMATCH`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Custom resolutions blocked 🐞 Bug ≡ Correctness
Description
verifyLockfileResolutions() now rejects any registry-style depPath (name@semver) whose snapshot
resolution has a non-"variations" type field, which includes type: 'custom:*' resolutions. This
can make installs fail with ERR_PNPM_RESOLUTION_SHAPE_MISMATCH for lockfiles produced/used by
custom resolvers that keep semver-shaped keys.
Code

installing/deps-installer/src/install/verifyLockfileResolutions.ts[R245-274]

+function collectResolutionShapeViolations (lockfile: LockfileObject): ResolutionPolicyViolation[] {
+  const violations: ResolutionPolicyViolation[] = []
+  for (const [depPath, snapshot] of Object.entries(lockfile.packages ?? {})) {
+    const { name, version, nonSemverVersion } = parse(depPath)
+    if (name == null || version == null || nonSemverVersion != null) continue
+    if (isRegistryShapedResolution(snapshot.resolution)) continue
+    violations.push({
+      name,
+      version,
+      resolution: snapshot.resolution as Resolution,
+      code: RESOLUTION_SHAPE_MISMATCH_VIOLATION_CODE,
+      reason: 'a registry-style dependency path is backed by a non-registry resolution',
+    })
+  }
+  return violations
+}
+
+function isRegistryShapedResolution (resolution: unknown): boolean {
+  if (resolution == null) return true
+  if (typeof resolution !== 'object') return false
+  const { type, gitHosted, variants } = resolution as { type?: unknown, gitHosted?: unknown, variants?: unknown }
+  if (type === 'variations') {
+    return Array.isArray(variants) && variants.every(
+      (variant) => isRegistryShapedResolution((variant as { resolution?: unknown })?.resolution)
+    )
+  }
+  if (type != null) return false
+  if (gitHosted === true) return false
+  return true
+}
Evidence
The shape gate flags semver depPaths unless the snapshot resolution is considered “registry-shaped”;
isRegistryShapedResolution() currently returns false for any resolution object with type != null
(except type: 'variations'), which includes custom:*. Custom resolutions are a supported
lockfile shape, and there is test coverage with a semver key foo@1.0.0 using `resolution: { type:
'custom:cdn', ... }`; since mutateModules runs verifyLockfileResolutions during install and the
structural pass runs even with no verifiers, such lockfiles will now be rejected outright.

installing/deps-installer/src/install/verifyLockfileResolutions.ts[245-274]
lockfile/types/src/index.ts[140-156]
installing/deps-installer/test/install/checkCustomResolverForceResolve.ts[208-227]
installing/deps-installer/src/install/index.ts[356-407]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new structural lockfile gate (`collectResolutionShapeViolations` / `isRegistryShapedResolution`) treats any `resolution.type` (other than `type: 'variations'`) as non-registry and throws `ERR_PNPM_RESOLUTION_SHAPE_MISMATCH` for semver depPath keys. This unintentionally blocks semver-keyed entries that use custom resolver resolutions (`type: 'custom:…'`), breaking installs that rely on custom resolvers.
## Issue Context
- Lockfile types explicitly support custom resolutions via `type: `custom:${string}`.
- There is existing test coverage constructing semver keys with `resolution.type === 'custom:cdn'`.
- `mutateModules()` calls `verifyLockfileResolutions()` early in install; the new structural pass runs even when `verifiers` is empty.
## Fix Focus Areas
- installing/deps-installer/src/install/verifyLockfileResolutions.ts[245-274]
### Suggested implementation directions (pick one, but be explicit)
1) **Compatibility-preserving exemption**: treat `type` values starting with `custom:` as “registry-shaped” for the purpose of this structural pass (or skip the structural check for them), so existing custom resolver lockfiles don’t hard-fail.
2) **Security-preserving redesign**: keep rejecting semver-keyed custom resolutions, but then update/require the custom resolver contract (and lockfile writer) to ensure custom resolver entries use non-registry depPath keys (so they are untrusted and must be approved by depPath). If you choose this, add a clear user-facing migration/error hint documenting the required lockfile key shape.
3) **Best-of-both**: exempt `custom:` from the structural throw, but ensure allowBuild’s trust decision is overridden to `trustPackageIdentity: false` for custom resolutions (requires plumbing resolution info into allowBuild call sites).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Hint leaks credentials 🐞 Bug ⛨ Security
Description
preparePackage() now interpolates the synthesized depPath (name + pkgResolutionId) into the error
hint, which can include HTTPS userinfo tokens from git URLs and leak them into terminals/CI logs.
This is provable because git resolution IDs intentionally preserve auth tokens in the repo/id
string.
Code

exec/prepare-package/src/index.ts[R36-49]

+  // If allowBuild is undefined or returns false, block the build.
+  // The depPath is synthesized from the resolution id rather than read from
+  // a lockfile; the manifest comes from the fetched artifact itself, so its
+  // name and version are never a trusted package identity.
+  const depPath = `${manifest.name}@${opts.pkgResolutionId}` as DepPath
+  if (!opts.allowBuild?.(depPath, { trustPackageIdentity: false })) {
throw new PnpmError(
'GIT_DEP_PREPARE_NOT_ALLOWED',
`The git-hosted package "${manifest.name}@${manifest.version}" needs to execute build scripts but is not in the "allowBuilds" allowlist.`,
{
hint: `Add the package to "allowBuilds" in your project's pnpm-workspace.yaml to allow it to run scripts. For example:
allowBuilds:
-  ${manifest.name}: true`,
+  ${depPath}: true`,
}
Evidence
The hint string directly embeds depPath, and git resolution IDs can legally contain embedded auth
tokens, so the thrown error can print secrets.

exec/prepare-package/src/index.ts[30-49]
fetching/git-fetcher/src/index.ts[46-54]
resolving/git-resolver/test/createGitHostedPkgId.test.ts[4-8]
resolving/git-resolver/test/index.ts[569-585]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`exec/prepare-package` includes `${depPath}` in the thrown `PnpmError` hint. For git deps, `depPath` includes `pkgResolutionId`, which can contain URL userinfo (tokens/passwords), causing credential leakage in user-facing output (terminal + CI logs).
## Issue Context
- `preparePackage()` synthesizes `depPath = `${manifest.name}@${opts.pkgResolutionId}`` and interpolates it into the hint.
- `git-fetcher` passes `pkgResolutionId: createGitHostedPkgId(resolution)`.
- `createGitHostedPkgId` (and `resolveFromGit`) preserve HTTPS userinfo tokens in IDs.
## Fix Focus Areas
- exec/prepare-package/src/index.ts[36-49]
- fetching/git-fetcher/src/index.ts[48-54]
- resolving/git-resolver/test/createGitHostedPkgId.test.ts[4-8]
- resolving/git-resolver/test/index.ts[569-585]
## Recommended fix approach
1. Introduce a small redaction helper (ideally shared) that strips URL userinfo from any embedded URL-like substrings in depPaths, e.g. `https://TOKEN:x-oa*******ic@github.com/...` -> `https://github.com/...`.
2. Use the redacted depPath in *all user-facing strings* (error message/hint), while keeping the non-redacted depPath for internal allowBuild checks.
3. Add a short note to the hint when redaction happened (e.g. “Credentials were redacted from this example; prefer using credential helpers instead of embedding tokens in URLs.”) to avoid confusing users when their lockfile depPath differs.
(If you want a stronger solution: normalize depPath comparisons in allowBuild policy to also ignore userinfo so users never need to put secrets into `allowBuilds` keys. That’s broader, but eliminates this class of leak entirely.)

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. Auto-writes secret depPaths 🐞 Bug ⛨ Security
Description
handleIgnoredBuilds() now derives allowBuilds keys via allowBuildKeyFromIgnoredBuild(), which
returns full depPaths for non-semver identities (git/tarball). If those depPaths contain URL
credentials, pnpm can persist secrets into pnpm-workspace.yaml as placeholder allowBuilds entries.
Code

building/policy/src/index.ts[R87-91]

+export function allowBuildKeyFromIgnoredBuild (depPath: string): string {
+  const normalizedDepPath = dp.removePeersSuffix(depPath)
+  const parsed = dp.parse(normalizedDepPath)
+  if (parsed.nonSemverVersion != null || parsed.name == null) return normalizedDepPath
+  return parsed.name
Evidence
The config-write path uses allowBuildKeyFromIgnoredBuild() for ignored builds; that function returns
raw depPaths for non-semver identities, and git depPath IDs are shown (by tests) to include auth
tokens in the URL userinfo section.

installing/commands/src/handleIgnoredBuilds.ts[27-46]
building/policy/src/index.ts[82-92]
resolving/git-resolver/test/index.ts[569-585]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The install flow can auto-write `allowBuilds` entries derived from ignored builds. For non-semver depPaths (git/tarball), `allowBuildKeyFromIgnoredBuild()` returns the full depPath, which may include URL userinfo credentials; `handleIgnoredBuilds` then writes these keys into `pnpm-workspace.yaml`.
## Issue Context
- `allowBuildKeyFromIgnoredBuild()` returns `normalizedDepPath` when `parsed.nonSemverVersion != null`.
- `handleIgnoredBuilds` writes placeholder `allowBuilds` entries for any missing keys.
- Git resolver IDs can include auth tokens embedded in URLs, so depPaths can contain secrets.
## Fix Focus Areas
- building/policy/src/index.ts[87-91]
- installing/commands/src/handleIgnoredBuilds.ts[27-46]
- resolving/git-resolver/test/index.ts[569-585]
## Recommended fix approach
1. Detect credential-bearing depPaths (URL userinfo) before returning/writing allowBuild keys.
2. For safety, do **not** auto-write such keys into `pnpm-workspace.yaml`. Instead:
- log a warning explaining that an ignored-build key was omitted because it contained URL credentials,
- instruct users to use credential helpers and/or manually add a sanitized key.
3. Optionally (more complete), add the same userinfo-stripping normalization to allowBuild depPath comparisons (both when ingesting config keys and when evaluating runtime depPaths), so users can approve by a credential-free depPath key.
This keeps the hardening behavior while preventing silent persistence of secrets into workspace config.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


11. Workspace builds untrusted 🐞 Bug ≡ Correctness
Description
building/after-install constructs AllowBuildContext from only depPath+resolution, so directory
(workspace) packages are always treated as untrusted and package-name allowBuilds approvals won’t
apply in rebuild/global-virtual-store flows. This can cause "pnpm rebuild" (and GVS hashing
decisions) to skip lifecycle scripts for workspace packages even when allowBuilds grants approval by
name.
Code

building/after-install/src/index.ts[R349-352]

+    switch (_allowBuild(pkgName, version, createAllowBuildContext({
+      depPath,
+      resolution: pkgSnapshots[depPath].resolution,
+    }))) {
Evidence
The rebuild flow passes only depPath+resolution into createAllowBuildContext. The trust function
returns false for directory resolutions when resolvedVia is absent, and lockfile snapshot types
don’t include resolvedVia, so lockfile-based flows can’t mark workspace identities trusted.

building/after-install/src/index.ts[346-352]
building/policy/src/index.ts[73-115]
lockfile/types/src/index.ts[34-67]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`building/after-install` passes `{ depPath, resolution }` into `createAllowBuildContext()`. For workspace packages, lockfile snapshots use a `DirectoryResolution` (`resolution.type === 'directory'`), and `isPackageIdentityTrustedForBuild()` treats any non-`variations` `resolution.type` as untrusted unless `resolvedVia` is explicitly provided. As a result, package-name `allowBuilds` entries won’t approve workspace packages in rebuild/global-virtual-store paths.
## Issue Context
- `createAllowBuildContext()` trusts identities via `resolvedVia` when present; otherwise it treats `resolution.type !== undefined` (except `variations`) as untrusted.
- Lockfile snapshot types currently don’t carry a `resolvedVia` field, so lockfile-based call sites cannot distinguish `workspace` from other directory-like sources.
## Fix Focus Areas
- building/after-install/src/index.ts[346-352]
- building/policy/src/index.ts[73-115]
- lockfile/types/src/index.ts[34-67]
## Suggested fix
1. Persist `resolvedVia` into lockfile `PackageSnapshot` (types + writer/reader), at least for values needed by this policy (`workspace`, `npm-registry`, `named-registry`, `jsr-registry`, etc.).
2. Update lockfile-based callers (`building/after-install`, `deps/graph-builder`, `deps/graph-hasher` iterators that use lockfile snapshots) to pass `resolvedVia` into `createAllowBuildContext({ depPath, resolution, resolvedVia })`.
3. Add a regression test covering a workspace package with `resolution.type='directory'` where a name-based allowBuilds entry successfully approves rebuild when `resolvedVia:'workspace'` is present.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


12. Patch hash stripped 🐞 Bug ⛨ Security
Description
normalizeBuildDepPath() uses dp.removeSuffix(), which removes the (patch_hash=...) segment in
addition to peer suffixes, so depPath-based allowBuilds rules cannot distinguish different patched
artifacts. This can cause an allow/deny entry meant for one patched instance to apply to a different
patch_hash variant of the same package.
Code

building/policy/src/index.ts[R80-82]

+export function normalizeBuildDepPath (depPath: string): string {
+  return dp.removeSuffix(depPath)
+}
Evidence
The policy normalizes depPaths via dp.removeSuffix(), but removeSuffix() explicitly removes
(patch_hash=...) (proven by unit tests). Patch hashes are appended during dependency resolution to
represent patched artifacts, and a dedicated helper (getPkgIdWithPatchHash) exists to preserve
patch identity while removing peer suffixes—indicating patch_hash is meant to remain part of the
identifier.

building/policy/src/index.ts[73-82]
building/policy/src/index.ts[51-66]
deps/path/src/index.ts[52-61]
deps/path/test/index.ts[117-153]
installing/deps-resolver/src/resolveDependencies.ts[1550-1555]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`normalizeBuildDepPath()` currently canonicalizes depPaths using `@pnpm/deps.path`'s `removeSuffix()`. That helper removes **both** peer suffixes and the `(patch_hash=...)` segment, collapsing distinct patched artifacts onto the same normalized depPath.
Because depPath-based allowBuilds approvals/denials are keyed by this normalized depPath, a rule intended for `foo@1.0.0(patch_hash=aaaa)` will also match `foo@1.0.0(patch_hash=bbbb)` (and any peer variants), which is an over-broad approval/deny.
### Issue Context
- `removeSuffix()` strips patch hashes (not just peers), but `getPkgIdWithPatchHash()` exists specifically to **keep** `(patch_hash=...)` while stripping peer suffixes.
- Patched dependencies are explicitly represented by appending `(patch_hash=...)` during resolution, so patch hashes are part of the artifact identity.
### Fix Focus Areas
- building/policy/src/index.ts[73-82]
- deps/path/src/index.ts[52-61]
- deps/path/test/index.ts[117-153]
- installing/deps-resolver/src/resolveDependencies.ts[1550-1555]
- installing/deps-installer/src/install/index.ts[1096-1124]
- building/commands/src/policy/approveBuilds.ts[232-237]
- building/commands/src/policy/getAutomaticallyIgnoredBuilds.ts[38-43]
- installing/commands/src/handleIgnoredBuilds.ts[49-58]
### Suggested change
1. Replace `normalizeBuildDepPath()` implementation to strip **only** peer suffixes while preserving `(patch_hash=...)`.
- Prefer `dp.getPkgIdWithPatchHash(depPath as DepPath)` (or introduce a new `removePeerSuffix()` helper in `@pnpm/deps.path` if the casting is undesirable).
2. Ensure all places that derive allowBuild keys from ignored builds or rebuild selectors use the same normalization (peer-only stripping, patch preserved).
- In particular, avoid `removeSuffix()` where the resulting string is used as an allowBuild key or rebuild selector for depPath-based approvals.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


13. Pacquet trusts missing metadata 🐞 Bug ⛨ Security
Description
pacquet computes trust_package_identity as true when packages metadata is absent/missing, so
package-name allowBuilds entries can still approve lifecycle scripts for git/tarball depPaths in
those scenarios. This contradicts the hardening intent (untrusted artifacts should require
depPath-based approval) and can re-enable scripts under incomplete lockfile/metadata conditions.
Code

pacquet/crates/package-manager/src/build_modules.rs[R642-651]

+        let trust_package_identity = packages
+            .and_then(|packages| packages.get(&metadata_key))
+            .is_none_or(package_identity_is_trusted);
+        let dep_path = metadata_key.to_string();
+        match allow_build_policy.check_with_context(
+            &name,
+            &version,
+            trust_package_identity,
+            Some(&dep_path),
+        ) {
Evidence
BuildModules.packages is optional, and the new trust computation uses is_none_or(...), which
returns true when metadata is missing. That makes check_with_context see trusted identity even
when it can’t be verified, enabling name-based approvals in exactly the cases this PR aims to
harden; the same pattern is duplicated in GVS hashing logic.

pacquet/crates/package-manager/src/build_modules.rs[271-283]
pacquet/crates/package-manager/src/build_modules.rs[640-651]
pacquet/crates/package-manager/src/virtual_store_layout.rs[201-217]
pacquet/crates/package-manager/src/build_modules/tests.rs[342-366]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
In pacquet, `trust_package_identity` currently defaults to `true` when lockfile `packages:` metadata is `None` or when the specific `metadata_key` is missing. This makes `AllowBuildPolicy::check_with_context` treat the package as trusted and allows package-name `allowBuilds` rules to approve lifecycle scripts for untrusted artifact identities (git/direct-tarball) whenever metadata is unavailable.
### Issue Context
- This PR’s goal is to require a *trusted* package identity before *package-name* allowBuild approvals apply to non-registry artifacts.
- pacquet already passes `dep_path` into the policy, so it has enough information to remain conservative when metadata is missing.
### Fix Focus Areas
- Ensure `trust_package_identity` is **false** when metadata is missing (or implement a safe fallback based on dep_path semantics), so name-based allowBuild approvals do not apply to untrusted artifacts.
- Apply the same fix in both build-script gating and GVS built-dep-paths computation.
- pacquet/crates/package-manager/src/build_modules.rs[642-651]
- pacquet/crates/package-manager/src/virtual_store_layout.rs[205-217]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

14. Shape-only verify cache disabled 🐞 Bug ➹ Performance
Description
verifyLockfileResolutions() now always scans the lockfile for resolution-shape violations but
returns early when verifiers.length === 0, so a successful shape-only run is never recorded to the
verification cache. Additionally, multiple call sites only pass/record the cache inputs when
resolutionVerifiers.length > 0, preventing stat-fast-path memoization for the common no-policy
case and redoing the scan on every install.
Code

installing/deps-installer/src/install/verifyLockfileResolutions.ts[R144-149]

+  const { candidates, shapeViolations } = collectCandidates(lockfile)
+  if (shapeViolations.length > 0) {
+    throw buildVerificationError(shapeViolations)
+  }
+  if (verifiers.length === 0) return
if (candidates.size === 0) {
Evidence
The code performs the shape scan (collectCandidates) and then returns early when there are no
verifiers, skipping any recordVerification path. Multiple callers also only enable cache
inputs/recording when resolutionVerifiers.length > 0, which prevents caching the shape-only work
that the PR introduced for verifier-free installs.

installing/deps-installer/src/install/verifyLockfileResolutions.ts[101-158]
installing/deps-installer/src/install/index.ts[387-399]
installing/deps-installer/src/install/recordLockfileVerified.ts[28-36]
installing/deps-installer/src/install/writeWantedLockfileAndRecordVerified.ts[18-40]
installing/deps-installer/src/install/writeLockfilesAndRecordVerified.ts[20-46]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`verifyLockfileResolutions()` now always performs the resolution-shape scan, but in the common case where `verifiers` is empty it returns immediately after the scan, without persisting a cache record. Separately, several install/write call sites only compute/pass `lockfilePath` and only record verification when `resolutionVerifiers.length > 0`, which makes it impossible to use the verification cache for shape-only checks.
This causes repeated installs (especially in large repos) to re-scan the lockfile every time, even when `cacheDir` is configured.
## Issue Context
- The PR adds `withResolutionShapeCacheIdentity()` and always runs `collectCandidates()` to enforce the shape invariant.
- But cache lookup/recording is currently gated by “has verifiers”, which no longer matches the work being done.
## Fix Focus Areas
- installing/deps-installer/src/install/verifyLockfileResolutions.ts[101-158]
- installing/deps-installer/src/install/index.ts[387-399]
- installing/deps-installer/src/install/recordLockfileVerified.ts[28-36]
- installing/deps-installer/src/install/writeWantedLockfileAndRecordVerified.ts[18-40]
- installing/deps-installer/src/install/writeLockfilesAndRecordVerified.ts[20-46]
## What to change
1. **Allow cache usage even when `verifiers` is empty**
- In `mutateModules` (install/index.ts), compute `wantedLockfilePath` whenever `cacheDir` is set (and a wanted lockfile exists), not only when `resolutionVerifiers.length > 0`.
2. **Record shape-only success**
- In `verifyLockfileResolutions`, after the shape scan passes and `verifiers.length === 0`, if `cache` is enabled, call `recordVerification()` using `cacheVerifiers` (which should include only the resolution-shape identity in this case), then return.
3. **Record post-resolution lockfiles under the shape identity**
- In `recordLockfileVerified` / `write*Lockfile*AndRecordVerified`, remove the `resolutionVerifiers.length > 0` gating and allow recording a cache entry even when there are zero verifiers, using `withResolutionShapeCacheIdentity([])`.
## Acceptance criteria
- With `resolutionVerifiers=[]` and cache enabled, the first run records a cache entry and subsequent runs can hit the stat-fast-path.
- Existing behavior with non-empty verifiers remains unchanged.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


15. Extra verify work always 🐞 Bug ➹ Performance
Description
verifyLockfileResolutions() calls collectCandidates() (which builds a dedup map and JSON.stringifys
each snapshot.resolution) before checking verifiers.length, so runs this heavier work even when
there are no active verifiers and only the resolution-shape invariant needs checking. This adds
avoidable CPU overhead on verifier-free installs (likely common) for large lockfiles.
Code

installing/deps-installer/src/install/verifyLockfileResolutions.ts[R131-139]

// A degenerate lockfile where every snapshot fails the
// name/version extraction (so candidates is empty) skips emission
// entirely — no work, no noise.
-  const candidates = collectCandidates(lockfile)
+  const { candidates, shapeViolations } = collectCandidates(lockfile)
+  if (shapeViolations.length > 0) {
+    throw buildVerificationError(shapeViolations)
+  }
+  if (verifiers.length === 0) return
if (candidates.size === 0) {
Evidence
The verifier-empty early return happens after collectCandidates(lockfile) is executed, and
collectCandidates constructs a dedupe key using JSON.stringify(snapshot.resolution) for every
package snapshot; when verifiers.length === 0, that dedupe/serialization work is not used.

installing/deps-installer/src/install/verifyLockfileResolutions.ts[127-139]
installing/deps-installer/src/install/verifyLockfileResolutions.ts[282-309]

Agent prompt
...

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 9, 2026

Copy link
Copy Markdown

PR Summary by Qodo

Harden allowBuilds approvals for git/tarball artifacts using trusted identity + depPath keys
🐞 Bug fix 🧪 Tests ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

Walkthroughs

Description
• Require trusted package identity before name-based allowBuilds can permit lifecycle scripts.
• Allow explicit approvals for git/tarball artifacts via peer-suffix-free lockfile depPath keys.
• Propagate the policy through install/build/fetch flows (and pacquet), with added coverage.
Diagram
graph TD
  cfg["Workspace allowBuilds config"] --> policy["building.policy: allowBuild + context"] --> install["Installer/build flows"]
  policy --> hash["Graph hash/builder"]
  fetch["Git/Tarball fetchers"] --> prep["preparePackage gate"] --> policy
  pacq["pacquet: mirrored policy"] --> policy
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Split artifact approvals into a dedicated map (e.g. allowBuildArtifacts)
  • ➕ Avoids overloading allowBuilds with two key types (names vs depPaths).
  • ➕ Makes intent explicit and prevents accidental depPath/name collisions.
  • ➖ New config surface area and migration complexity.
  • ➖ Still requires threading artifact identity context through the pipeline (same plumbing cost).
2. Persist explicit approvals in lockfile/modules manifest only
  • ➕ Approvals become immutable per-resolved artifact; reduces config drift.
  • ➕ Naturally ties approval to the exact artifact identity/depPath.
  • ➖ Harder for users to manage centrally across workspaces.
  • ➖ More invasive change to lockfile semantics and upgrade paths.

Recommendation: Keep the PR’s approach: it preserves existing name-based workflow for trusted registry/workspace packages while closing the artifact-spoofing gap by requiring depPath-specific keys for git/tarball sources. The added context plumbing is unavoidable for correctness, and mirroring in pacquet reduces policy divergence.

Grey Divider

File Changes

Enhancement (6)
index.ts Harden allowBuilds evaluation with trust + depPath-aware rules +116/-10

Harden allowBuilds evaluation with trust + depPath-aware rules

• Introduces allowBuild context creation (depPath normalization + trustPackageIdentity evaluation) and splits allowBuilds rules into package-spec vs depPath buckets. Adds allowBuildKeyFromIgnoredBuild to ensure approvals for git/tarball artifacts use depPath keys rather than package names, and enforces trusted identity for name-based approvals.

building/policy/src/index.ts


config.ts Extend AllowBuild signature to accept optional context +6/-1

Extend AllowBuild signature to accept optional context

• Adds AllowBuildContext (depPath + trustPackageIdentity) and updates the AllowBuild function type to accept the context as an optional third argument.

core/types/src/config.ts


package.json Reuse git-resolver for stable git package IDs +1/-0

Reuse git-resolver for stable git package IDs

• Adds @pnpm/resolving.git-resolver dependency so the fetcher can generate canonical pkgResolutionId values for allowBuild depPath context.

fetching/git-fetcher/package.json


index.ts Include resolution and resolvedVia in graph package meta iteration +3/-3

Include resolution and resolvedVia in graph package meta iteration

• Extends iterated graph metadata to include resolution and resolvedVia so downstream hashing/build decisions can evaluate trust context.

installing/deps-resolver/src/index.ts


resolveDependencies.ts Plumb resolvedVia from registry response into resolved packages +2/-0

Plumb resolvedVia from registry response into resolved packages

• Adds optional resolvedVia to ResolvedPackage and populates it from pkgResponse, enabling trust decisions based on resolution source.

installing/deps-resolver/src/resolveDependencies.ts


build_modules.rs Implement depPath-based approvals and trusted-identity gating in pacquet policy +137/-10

Implement depPath-based approvals and trusted-identity gating in pacquet policy

• Extends AllowBuildPolicy to maintain allowed/disallowed depPath sets, normalizes depPaths (remove_suffix), and introduces check_with_context to require trusted identity for name-based approvals while allowing explicit depPath approvals. Adds trust evaluation based on lockfile resolution and uses it when deciding whether to run scripts and when computing GVS slot selection.

pacquet/crates/package-manager/src/build_modules.rs


Bug fix (20)
index.ts Support depPath selectors and pass allowBuild identity context during rebuild +55/-5

Support depPath selectors and pass allowBuild identity context during rebuild

• Extends rebuild package selection to accept depPath selectors (including peer-suffix-free matches). Threads depPath+resolution into allowBuild via createAllowBuildContext, and improves global virtual store dir selection by discovering linked GVS directories via symlink targets when the preferred path is absent.

building/after-install/src/index.ts


approveBuilds.ts Use centralized allowBuild key derivation when approving ignored builds +2/-2

Use centralized allowBuild key derivation when approving ignored builds

• Replaces ad-hoc depPath parsing with allowBuildKeyFromIgnoredBuild so approvals are keyed by name for trusted semver deps and by depPath for artifact identities.

building/commands/src/policy/approveBuilds.ts


getAutomaticallyIgnoredBuilds.ts Derive automatically ignored build keys using allowBuildKeyFromIgnoredBuild +2/-2

Derive automatically ignored build keys using allowBuildKeyFromIgnoredBuild

• Switches ignored build name computation to the shared allowBuildKeyFromIgnoredBuild helper to avoid incorrectly treating artifact depPaths as package-name approvals.

building/commands/src/policy/getAutomaticallyIgnoredBuilds.ts


index.ts Pass allowBuild context when deciding whether to run lifecycle scripts +2/-1

Pass allowBuild context when deciding whether to run lifecycle scripts

• Updates buildModules to call allowBuild(name, version, createAllowBuildContext(node)) so name-based approvals only apply when identity is trusted, while depPath-based approvals can still work for artifacts.

building/during-install/src/index.ts


lockfileToDepGraph.ts Use allowBuild context when deciding global-virtual-store fast path +5/-1

Use allowBuild context when deciding global-virtual-store fast path

• Updates allowBuild checks used for global virtual store build markers to pass depPath and package snapshot resolution into createAllowBuildContext.

deps/graph-builder/src/lockfileToDepGraph.ts


index.ts Thread allowBuild identity context through built-depPath computation +13/-4

Thread allowBuild identity context through built-depPath computation

• Extends PkgMeta entries to include snapshot/resolution/resolvedVia and uses createAllowBuildContext when computing built depPaths. Ensures graph hashing and build-state decisions align with the hardened allowBuilds policy.

deps/graph-hasher/src/index.ts


index.ts Treat fetched manifests as untrusted and require depPath-based approvals +10/-3

Treat fetched manifests as untrusted and require depPath-based approvals

• Adds pkgResolutionId option and always calls allowBuild with trustPackageIdentity:false, constructing a depPath-like identity when available. Updates the error hint to recommend approving by depPath for artifact-based dependencies.

exec/prepare-package/src/index.ts


index.ts Pass canonical pkgResolutionId into preparePackage for allowBuild context +2/-0

Pass canonical pkgResolutionId into preparePackage for allowBuild context

• Uses createGitHostedPkgId(resolution) and passes it as pkgResolutionId so preparePackage can build a stable depPath identity for git-hosted artifacts.

fetching/git-fetcher/src/index.ts


gitHostedTarballFetcher.ts Provide pkgResolutionId for git-hosted tarballs (tarball + optional path) +9/-0

Provide pkgResolutionId for git-hosted tarballs (tarball + optional path)

• Constructs a stable pkgResolutionId from the tarball URL and path fragment and passes it into preparePackage to support depPath-based approvals for git-hosted tarball artifacts.

fetching/tarball-fetcher/src/gitHostedTarballFetcher.ts


handleIgnoredBuilds.ts Write allowBuilds entries using correct approval keys (name vs depPath) +2/-2

Write allowBuilds entries using correct approval keys (name vs depPath)

• Replaces depPath name parsing with allowBuildKeyFromIgnoredBuild so the generated allowBuilds config uses depPath keys for artifacts and names for trusted registry packages.

installing/commands/src/handleIgnoredBuilds.ts


index.ts Detect revoked build approvals and use identity context across flows +26/-9

Detect revoked build approvals and use identity context across flows

• Threads createAllowBuildContext into build/side-effects decisions, updates unignored-build rebuilding to consult the current lockfile snapshot resolution, and detects revoked approvals even for non-semver depPaths by evaluating the new trust-aware policy against prior approvals.

installing/deps-installer/src/install/index.ts


link.ts Use allowBuild context for side-effects cache key selection +2/-1

Use allowBuild context for side-effects cache key selection

• Updates the side-effects cache read path to call allowBuild with createAllowBuildContext(depNode), aligning caching/build decisions with the hardened policy.

installing/deps-installer/src/install/link.ts


index.ts Use allowBuild context for side-effects cache read during restore +2/-2

Use allowBuild context for side-effects cache read during restore

• Updates restore-time side-effects cache selection to call allowBuild with createAllowBuildContext(depNode).

installing/deps-restorer/src/index.ts


linkHoistedModules.ts Use allowBuild context for side-effects cache read when linking hoisted modules +2/-1

Use allowBuild context for side-effects cache read when linking hoisted modules

• Updates hoisted linking side-effects logic to call allowBuild with createAllowBuildContext(depNode).

installing/deps-restorer/src/linkHoistedModules.ts


fetcher.rs Thread trust + depPath into pacquet prepare-package allow_build closure +6/-3

Thread trust + depPath into pacquet prepare-package allow_build closure

• Updates allow_build closure shape to accept trust and dep_path, and passes the package_id as dep_path into prepare_package options to support depPath-specific approvals for untrusted manifests.

pacquet/crates/git-fetcher/src/fetcher.rs


prepare_package.rs Mirror upstream preparePackage trust semantics in pacquet +10/-7

Mirror upstream preparePackage trust semantics in pacquet

• Changes allow_build signature to include trust and dep_path, adds dep_path to PreparePackageOptions, and enforces trust=false for manifest-derived name/version checks unless depPath-specific policy allows the build.

pacquet/crates/git-fetcher/src/prepare_package.rs


tarball_fetcher.rs Thread trust + depPath into git-hosted tarball prepare-package gate +6/-3

Thread trust + depPath into git-hosted tarball prepare-package gate

• Updates tarball fetcher allow_build plumbing to the new signature and passes package_id as dep_path into prepare_package options.

pacquet/crates/git-fetcher/src/tarball_fetcher.rs


install_package_by_snapshot.rs Pass trust + depPath context into allow_build closure during install +5/-1

Pass trust + depPath context into allow_build closure during install

• Updates the allow_build closure used by installers to call check_with_context and default-deny when the policy returns None.

pacquet/crates/package-manager/src/install_package_by_snapshot.rs


virtual_store_layout.rs Apply trust-aware allowBuild policy when determining built snapshots for GVS +13/-2

Apply trust-aware allowBuild policy when determining built snapshots for GVS

• Updates built-snapshot detection for GVS slotting to evaluate check_with_context using trust derived from metadata/resolution and a depPath string derived from the snapshot key.

pacquet/crates/package-manager/src/virtual_store_layout.rs


createGitHostedPkgId.ts Normalize SCP-style git repo URLs when creating pkg resolution IDs +7/-1

Normalize SCP-style git repo URLs when creating pkg resolution IDs

• Adds normalization to convert SCP-like forms (git@host:org/repo.git) into ssh:// URLs before building the canonical git+...#commit(&path:...) pkgResolutionId.

resolving/git-resolver/src/createGitHostedPkgId.ts


Tests (13)
index.ts Update rebuild CLI tests to approve git artifacts by depPath +6/-4

Update rebuild CLI tests to approve git artifacts by depPath

• Adjusts tests to capture the git dependency depPath from pendingBuilds and uses it as the allowBuilds key instead of approving by package name.

building/commands/test/build/index.ts


index.ts Add coverage for trusted-identity enforcement and depPath approvals +110/-1

Add coverage for trusted-identity enforcement and depPath approvals

• Adds tests verifying that package-name allowBuilds require trusted identity, depPath allowBuild keys can allow untrusted artifact identities, and patch hashes are preserved for depPath matching. Adds validation that invalid version unions throw errors.

building/policy/test/index.ts


package.json Pin git protocol fixture to a specific commit +1/-1

Pin git protocol fixture to a specific commit

• Updates the fixture dependency to include a commit hash to ensure a stable depPath/identity for allowBuilds behavior and tests.

deps/compliance/commands/test/licenses/fixtures/with-git-protocol-peer-deps/package.json


index.ts Approve git protocol dependency by depPath in license command tests +3/-1

Approve git protocol dependency by depPath in license command tests

• Updates allowBuilds configuration in the test to use the git-hosted tarball depPath key rather than the package name.

deps/compliance/commands/test/licenses/index.ts


allowBuildIdentity.test.ts Test identity trust context for registry vs direct tarball resolutions +53/-0

Test identity trust context for registry vs direct tarball resolutions

• Adds a new test verifying that allowBuild is invoked with trustPackageIdentity=true for registry tarball metadata and false for direct tarball identities, and that depPath is passed through unchanged.

deps/graph-hasher/test/allowBuildIdentity.test.ts


index.ts Add test for rejecting untrusted manifest identity when policy requires trust +11/-0

Add test for rejecting untrusted manifest identity when policy requires trust

• Introduces an allowTrustedBuildsOnly gate and validates preparePackage rejects when trustPackageIdentity is false.

exec/prepare-package/test/index.ts


lifecycleScripts.ts Add end-to-end coverage for depPath-only approvals and drain log stream +88/-2

Add end-to-end coverage for depPath-only approvals and drain log stream

• Adds tests proving package-name allowBuilds does not enable scripts for direct tarballs, while depPath approval does. Improves git prepare test by generating a local git repo dependency and drains the global log stream to prevent cross-test reporter interference.

installing/deps-installer/test/install/lifecycleScripts.ts


tests.rs Add pacquet git fetcher tests for untrusted manifest rejection + depPath allow +97/-7

Add pacquet git fetcher tests for untrusted manifest rejection + depPath allow

• Updates allow_build test helpers to new signature and adds tests verifying untrusted manifests are rejected unless explicitly allowed by depPath.

pacquet/crates/git-fetcher/src/fetcher/tests.rs


tests.rs Add pacquet prepare-package tests for trust-only and depPath-only policies +81/-1

Add pacquet prepare-package tests for trust-only and depPath-only policies

• Adds targeted tests that reject untrusted manifest identity under trust-only policies and allow it when depPath is explicitly approved.

pacquet/crates/git-fetcher/src/prepare_package/tests.rs


tests.rs Update tarball fetcher tests for new allow_build signature +6/-3

Update tarball fetcher tests for new allow_build signature

• Adjusts deny_all_builds helper to match the new (name, version, trust, dep_path) signature.

pacquet/crates/git-fetcher/src/tarball_fetcher/tests.rs


tests.rs Align pacquet policy tests with upstream trust + depPath behavior +77/-14

Align pacquet policy tests with upstream trust + depPath behavior

• Reworks policy_from_specs to use AllowBuildPolicy::from_config (matching runtime parsing) and adds tests for identity trust requirements, depPath-based approvals, and dangerouslyAllowAll behavior with untrusted identities.

pacquet/crates/package-manager/src/build_modules/tests.rs


tests.rs Test that untrusted source depPaths don’t become name-allowed without metadata +36/-1

Test that untrusted source depPaths don’t become name-allowed without metadata

• Adds coverage ensuring missing metadata keeps source-like depPaths untrusted for GVS decisions, preventing name-based approvals from affecting source artifacts when metadata is absent.

pacquet/crates/package-manager/src/virtual_store_layout/tests.rs


createGitHostedPkgId.test.ts Add test for SCP-style repo normalization with path fragment +1/-0

Add test for SCP-style repo normalization with path fragment

• Extends test matrix to cover git@example.com:org/repo.git normalization to git+ssh://... with &path:... appended.

resolving/git-resolver/test/createGitHostedPkgId.test.ts


Documentation (1)
tough-allow-builds-identities.md Document allowBuilds identity hardening and depPath-based approvals +17/-0

Document allowBuilds identity hardening and depPath-based approvals

• Adds a changeset describing the new requirement for trusted identities when approving by package name. Documents explicit approvals for git/tarball artifacts via peer-suffix-free lockfile depPath keys.

.changeset/tough-allow-builds-identities.md


Other (13)
package.json Add building.policy dependency to building commands +1/-0

Add building.policy dependency to building commands

• Adds @pnpm/building.policy as a direct dependency so CLI policy commands can reuse shared allowBuild key logic.

building/commands/package.json


tsconfig.json Reference building/policy project from commands tsconfig +3/-0

Reference building/policy project from commands tsconfig

• Adds a TypeScript project reference so building/commands can import shared policy helpers.

building/commands/tsconfig.json


package.json Add building.policy dependency to during-install builder +1/-0

Add building.policy dependency to during-install builder

• Adds @pnpm/building.policy so buildModules can construct allowBuild context for each graph node.

building/during-install/package.json


tsconfig.json Reference building/policy project from during-install tsconfig +3/-0

Reference building/policy project from during-install tsconfig

• Adds a TypeScript project reference to building/policy.

building/during-install/tsconfig.json


package.json Add building.policy dependency to graph-builder +1/-0

Add building.policy dependency to graph-builder

• Adds @pnpm/building.policy to allow graph-building logic to pass allowBuild context based on depPath and resolution.

deps/graph-builder/package.json


tsconfig.json Reference building/policy project from graph-builder tsconfig +3/-0

Reference building/policy project from graph-builder tsconfig

• Adds a TypeScript project reference to building/policy.

deps/graph-builder/tsconfig.json


package.json Add building.policy dependency to graph-hasher +1/-0

Add building.policy dependency to graph-hasher

• Adds @pnpm/building.policy so hashed-graph iteration can include identity context in build decisions.

deps/graph-hasher/package.json


tsconfig.json Reference building/policy project from graph-hasher tsconfig +3/-0

Reference building/policy project from graph-hasher tsconfig

• Adds a TypeScript project reference to building/policy.

deps/graph-hasher/tsconfig.json


tsconfig.json Add TypeScript project reference to resolving/git-resolver +3/-0

Add TypeScript project reference to resolving/git-resolver

• Adds a TS project reference for the new createGitHostedPkgId import.

fetching/git-fetcher/tsconfig.json


package.json Add building.policy dependency to installing commands +1/-0

Add building.policy dependency to installing commands

• Adds @pnpm/building.policy so ignored-build handling can use shared allowBuild key logic.

installing/commands/package.json


tsconfig.json Reference building/policy project from installing commands tsconfig +3/-0

Reference building/policy project from installing commands tsconfig

• Adds a TS project reference to building/policy.

installing/commands/tsconfig.json


pnpm-lock.yaml Bump shell-quote to 1.8.4 via overrides +23/-4

Bump shell-quote to 1.8.4 via overrides

• Updates the lockfile to reflect the override to shell-quote@1.8.4, replacing 1.8.3 in resolved packages and snapshots.

pnpm-lock.yaml


pnpm-workspace.yaml Override shell-quote to 1.8.4 for CVE fix +2/-0

Override shell-quote to 1.8.4 for CVE fix

• Pins shell-quote to 1.8.4 with a comment referencing the GHSA/CVE affecting <=1.8.3.

pnpm-workspace.yaml


Grey Divider

Qodo Logo

Comment thread pacquet/crates/package-manager/src/build_modules.rs Outdated
@zkochan zkochan force-pushed the security/cand-pnpm-123-allowbuilds-deppath branch from 17d1bfc to ba4cc44 Compare June 9, 2026 20:23
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit ba4cc44

@zkochan zkochan marked this pull request as draft June 9, 2026 20:23

@coderabbitai coderabbitai 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.

Actionable comments posted: 5

🧹 Nitpick comments (1)
building/commands/src/policy/approveBuilds.ts (1)

232-237: ⚡ Quick win

Extract allowBuildKeyFromIgnoredBuild into a shared utility.

This same key-normalization logic is duplicated in building/commands/src/policy/getAutomaticallyIgnoredBuilds.ts and installing/commands/src/handleIgnoredBuilds.ts. Centralizing it avoids drift between approval matching and ignored-build serialization behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@building/commands/src/policy/approveBuilds.ts` around lines 232 - 237,
Extract the duplicated normalization logic currently implemented as
allowBuildKeyFromIgnoredBuild into a single shared utility function (e.g.,
export function normalizeIgnoredBuildKey) and replace the inline implementation
in approveBuilds.ts, getAutomaticallyIgnoredBuilds.ts, and
handleIgnoredBuilds.ts with calls to that new exported helper; preserve the
current behavior (call removeSuffix, parse, return normalizedDepPath when
parsed.nonSemverVersion != null or parsed.name == null, otherwise return
parsed.name), export the helper from a common module used by the three callers,
and update imports in the three files accordingly so approval matching and
ignored-build serialization use the exact same function.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@building/after-install/src/index.ts`:
- Around line 376-379: The fallback to find a linked global virtual store
directory ignores custom globalVirtualStoreDir because findLinkedGvsDir
hardcodes the links path; update findLinkedGvsDir to accept the current
globalVirtualStoreDir (add a parameter, e.g. globalVirtualStoreDir) and use it
to construct the links lookup instead of a hard-coded path, then update calls
from gvsDirByDepPath.set (where preferredGvsDir, pkgMeta.depPath, pkgMeta.name
and ctx.projects are used) to pass the globalVirtualStoreDir; ensure the
fallback uses the returned linked path only when defined and otherwise keeps
preferredGvsDir, and apply the same change for the other occurrence mentioned.

In `@fetching/git-fetcher/src/index.ts`:
- Around line 112-119: createGitPkgResolutionId is incorrectly prefixing
https:// for SCP-style SSH git URLs (e.g. git@host:user/repo.git) because it
only checks for '://'; update createGitPkgResolutionId to detect SCP-style SSH
(e.g. strings starting like git@host: or matching /^[^@]+@[^:]+:/) and normalize
them to an ssh form (e.g. produce a git+ssh://... shape or reuse the same
normalization logic as hosted.sshurl()/hosted.ssh()) instead of adding https://;
also update the matching resolver-id generation path that builds IDs from
parsedSpec.fetchSpec to use the same detection/normalization and add a unit test
asserting that an input of git@host:user/repo.git yields a git+ssh:// (or
equivalent ssh-shaped) id with the correct #<commit> and &path when present.

In `@pacquet/crates/package-manager/src/build_modules.rs`:
- Around line 642-651: The current computation of trust_package_identity uses
is_none_or(package_identity_is_trusted) which treats missing metadata as
trusted; change it to default to untrusted by returning false when metadata is
absent. Replace the is_none_or-based expression with an explicit Option check
(e.g., use Option::map_or(false, |meta| package_identity_is_trusted(meta)) or
equivalent) on packages.and_then(|packages| packages.get(&metadata_key)) so
allow_build_policy.check_with_context receives false for unknown package
identity.

In `@pacquet/crates/package-manager/src/virtual_store_layout.rs`:
- Around line 208-217: The current trust logic uses
packages.and_then(...).is_none_or(package_identity_is_trusted) which treats
missing metadata as trusted; change it so missing metadata is untrusted by
default by evaluating trust_package_identity using a map_or(false,
package_identity_is_trusted) or is_some_and(package_identity_is_trusted) on
packages.and_then(...). Update the expression that sets trust_package_identity
(referencing packages, metadata_key, and package_identity_is_trusted) so
policy.check_with_context receives false for missing entries instead of true.

In `@pnpm-workspace.yaml`:
- Line 440: The dependency pin for shell-quote (shell-quote: 1.8.4) lacks a
comment explaining why it is pinned; add a short inline comment next to the
shell-quote: 1.8.4 entry in pnpm-workspace.yaml that references the security
advisory (GHSA-w7jw-789q-3m8p / CVE-2026-9277) and notes that 1.8.4 patches the
critical vulnerability present in <=1.8.3 so future maintainers understand the
pin.

---

Nitpick comments:
In `@building/commands/src/policy/approveBuilds.ts`:
- Around line 232-237: Extract the duplicated normalization logic currently
implemented as allowBuildKeyFromIgnoredBuild into a single shared utility
function (e.g., export function normalizeIgnoredBuildKey) and replace the inline
implementation in approveBuilds.ts, getAutomaticallyIgnoredBuilds.ts, and
handleIgnoredBuilds.ts with calls to that new exported helper; preserve the
current behavior (call removeSuffix, parse, return normalizedDepPath when
parsed.nonSemverVersion != null or parsed.name == null, otherwise return
parsed.name), export the helper from a common module used by the three callers,
and update imports in the three files accordingly so approval matching and
ignored-build serialization use the exact same function.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a0fcd8f4-682f-4484-bc08-7810888967f0

📥 Commits

Reviewing files that changed from the base of the PR and between 230df57 and 17d1bfc.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (39)
  • .changeset/tough-allow-builds-identities.md
  • building/after-install/src/index.ts
  • building/commands/src/policy/approveBuilds.ts
  • building/commands/src/policy/getAutomaticallyIgnoredBuilds.ts
  • building/commands/test/build/index.ts
  • building/during-install/package.json
  • building/during-install/src/index.ts
  • building/during-install/tsconfig.json
  • building/policy/src/index.ts
  • building/policy/test/index.ts
  • core/types/src/config.ts
  • deps/graph-builder/package.json
  • deps/graph-builder/src/lockfileToDepGraph.ts
  • deps/graph-builder/tsconfig.json
  • deps/graph-hasher/src/index.ts
  • deps/graph-hasher/test/allowBuildIdentity.test.ts
  • exec/prepare-package/src/index.ts
  • exec/prepare-package/test/index.ts
  • fetching/git-fetcher/src/index.ts
  • fetching/tarball-fetcher/src/gitHostedTarballFetcher.ts
  • installing/commands/src/handleIgnoredBuilds.ts
  • installing/deps-installer/src/install/index.ts
  • installing/deps-installer/src/install/link.ts
  • installing/deps-installer/test/install/lifecycleScripts.ts
  • installing/deps-resolver/src/index.ts
  • installing/deps-resolver/src/resolveDependencies.ts
  • installing/deps-restorer/src/index.ts
  • installing/deps-restorer/src/linkHoistedModules.ts
  • pacquet/crates/git-fetcher/src/fetcher.rs
  • pacquet/crates/git-fetcher/src/fetcher/tests.rs
  • pacquet/crates/git-fetcher/src/prepare_package.rs
  • pacquet/crates/git-fetcher/src/prepare_package/tests.rs
  • pacquet/crates/git-fetcher/src/tarball_fetcher.rs
  • pacquet/crates/git-fetcher/src/tarball_fetcher/tests.rs
  • pacquet/crates/package-manager/src/build_modules.rs
  • pacquet/crates/package-manager/src/build_modules/tests.rs
  • pacquet/crates/package-manager/src/install_package_by_snapshot.rs
  • pacquet/crates/package-manager/src/virtual_store_layout.rs
  • pnpm-workspace.yaml
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: ubuntu-latest / Node.js 24 / Test
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Lint and Test (windows-latest)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use Standard Style with trailing commas, prefer functions over classes, declare functions after they are used (relying on hoisting), limit functions to no more than two or three arguments, and use a single options object for functions needing more parameters
Follow import order: standard libraries first, then external dependencies (sorted alphabetically), then relative imports
Do not write comments that restate what the code already says; rename variables, split helpers, or move checks to more obvious places instead
Do not repeat documentation at call sites that already lives on the callee; update the JSDoc once and let every call site benefit
Use JSDoc for the function's contract (preconditions, postconditions, edge cases, why the function exists), not for re-narrating the function body
Do not record past implementation shape, refactor history, or removed code in comments; use git log and git blame for that information instead
Write comments only when the reason for code is non-obvious, a hidden invariant exists, a workaround for a known bug is needed, or an exception to surrounding pattern is deliberate

Files:

  • core/types/src/config.ts
  • installing/deps-installer/src/install/link.ts
  • deps/graph-hasher/test/allowBuildIdentity.test.ts
  • building/commands/src/policy/getAutomaticallyIgnoredBuilds.ts
  • installing/deps-resolver/src/index.ts
  • exec/prepare-package/src/index.ts
  • fetching/tarball-fetcher/src/gitHostedTarballFetcher.ts
  • installing/deps-restorer/src/linkHoistedModules.ts
  • installing/commands/src/handleIgnoredBuilds.ts
  • installing/deps-restorer/src/index.ts
  • deps/graph-builder/src/lockfileToDepGraph.ts
  • fetching/git-fetcher/src/index.ts
  • building/policy/test/index.ts
  • installing/deps-resolver/src/resolveDependencies.ts
  • building/during-install/src/index.ts
  • exec/prepare-package/test/index.ts
  • building/commands/src/policy/approveBuilds.ts
  • building/after-install/src/index.ts
  • deps/graph-hasher/src/index.ts
  • building/policy/src/index.ts
  • installing/deps-installer/src/install/index.ts
  • installing/deps-installer/test/install/lifecycleScripts.ts
  • building/commands/test/build/index.ts
**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use instanceof Error for checking if a caught error is an Error object in Jest tests; use util.types.isNativeError() instead to work across realms

Files:

  • deps/graph-hasher/test/allowBuildIdentity.test.ts
pacquet/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/*.rs: Log emissions are part of matching pnpm — when porting a function that fires pnpm:<channel> events through globalLogger, logger.debug(...), or streamParser.write(...), mirror the call site, payload, and ordering so @pnpm/cli.default-reporter parses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing the brand into a plain String or &str in Rust
If upstream TypeScript always validates before construction of a branded string, validate in the Rust wrapper too via TryFrom<String> and/or FromStr and do not provide an infallible public constructor
If upstream TypeScript never validates a branded string, just brand for type-safety in Rust by exposing an infallible From<String> constructor
If upstream TypeScript occasionally constructs a branded string without validation, expose from_str_unchecked in Rust as an escape hatch alongside the validating constructor
Match upstream serde behavior for branded strings crossing JSON, YAML, or INI boundaries by using #[serde(try_from = "String")] for deserialization and #[serde(into = "String")] for serialization
Derive simple conversions for branded strings using #[derive(derive_more::From)] and #[derive(derive_more::Into)] instead of handwriting impl blocks; use manual impl only when conversion needs custom logic
Model TypeScript string literal unions (like 'auto' | 'always' | 'never') as Rust enums instead of newtype wrappers, since the set of valid values is closed
Treat TypeScript string template literal types (like `${string}@${string}`) the same as branded string types in Rust, using a newtype wrapper with validation
Follow the code style guide in CODE_STYLE_GUIDE.md — imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching, pipe-trait, error handling, test layout, and cloning of Arc and Rc
Choose owned vs. borrowed parameters to minimize copies; widen to t...

Files:

  • pacquet/crates/package-manager/src/virtual_store_layout.rs
  • pacquet/crates/git-fetcher/src/tarball_fetcher.rs
  • pacquet/crates/git-fetcher/src/tarball_fetcher/tests.rs
  • pacquet/crates/package-manager/src/install_package_by_snapshot.rs
  • pacquet/crates/git-fetcher/src/fetcher.rs
  • pacquet/crates/git-fetcher/src/prepare_package.rs
  • pacquet/crates/git-fetcher/src/prepare_package/tests.rs
  • pacquet/crates/package-manager/src/build_modules/tests.rs
  • pacquet/crates/git-fetcher/src/fetcher/tests.rs
  • pacquet/crates/package-manager/src/build_modules.rs
🧠 Learnings (9)
📚 Learning: 2026-05-05T23:03:04.286Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11479
File: __utils__/scripts/package.json:6-9
Timestamp: 2026-05-05T23:03:04.286Z
Learning: The pattern cross-env NODE_OPTIONS="$NODE_OPTIONS ..." in package.json scripts is an established convention in the pnpm/pnpm repository and is used across many packages (e.g., fs/hard-link-dir, worker, __utils__/scripts). Do not flag this as a cross-platform issue in individual files; if a change is needed, apply it as a repo-wide change in a separate PR. Scope this guidance to all package.json files in the repo; use the minimatch pattern '**/package.json' to identify relevant files and review changes at the repository level rather than per-file.

Applied to files:

  • building/during-install/package.json
  • deps/graph-builder/package.json
📚 Learning: 2026-05-14T09:04:00.133Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11622
File: resolving/npm-resolver/test/publishedBy.test.ts:350-354
Timestamp: 2026-05-14T09:04:00.133Z
Learning: In the pnpm/pnpm repository, ESLint is the authoritative style linter. Do not raise review findings for missing trailing commas in multiline function calls (e.g., `fs.writeFileSync(...)`) when this repo’s ESLint configuration does not report them and lint passes. Prefer deferring to the ESLint results for this specific trailing-comma rule rather than enforcing it manually in code review.

Applied to files:

  • core/types/src/config.ts
  • installing/deps-installer/src/install/link.ts
  • deps/graph-hasher/test/allowBuildIdentity.test.ts
  • building/commands/src/policy/getAutomaticallyIgnoredBuilds.ts
  • installing/deps-resolver/src/index.ts
  • exec/prepare-package/src/index.ts
  • fetching/tarball-fetcher/src/gitHostedTarballFetcher.ts
  • installing/deps-restorer/src/linkHoistedModules.ts
  • installing/commands/src/handleIgnoredBuilds.ts
  • installing/deps-restorer/src/index.ts
  • deps/graph-builder/src/lockfileToDepGraph.ts
  • fetching/git-fetcher/src/index.ts
  • building/policy/test/index.ts
  • installing/deps-resolver/src/resolveDependencies.ts
  • building/during-install/src/index.ts
  • exec/prepare-package/test/index.ts
  • building/commands/src/policy/approveBuilds.ts
  • building/after-install/src/index.ts
  • deps/graph-hasher/src/index.ts
  • building/policy/src/index.ts
  • installing/deps-installer/src/install/index.ts
  • installing/deps-installer/test/install/lifecycleScripts.ts
  • building/commands/test/build/index.ts
📚 Learning: 2026-06-05T13:47:26.046Z
Learnt from: vsumner
Repo: pnpm/pnpm PR: 12190
File: installing/deps-installer/src/install/index.ts:2337-2343
Timestamp: 2026-06-05T13:47:26.046Z
Learning: In the pnpm/pnpm codebase, `PnpmError` automatically prefixes `err.code` with `ERR_PNPM_` when you pass a code that does not already start with `ERR_PNPM_` (it normalizes `this.code` via `code.startsWith('ERR_PNPM_') ? code : `ERR_PNPM_${code}``). Therefore, during code review you should NOT flag `new PnpmError(...)` call sites for passing a bare error code (e.g., `new PnpmError('FROZEN_STORE_INCOMPATIBLE_WITH_PNPR', ...)`); the resulting `err.code` will still be `ERR_PNPM_FROZEN_STORE_INCOMPATIBLE_WITH_PNPR`.

Applied to files:

  • core/types/src/config.ts
  • installing/deps-installer/src/install/link.ts
  • deps/graph-hasher/test/allowBuildIdentity.test.ts
  • building/commands/src/policy/getAutomaticallyIgnoredBuilds.ts
  • installing/deps-resolver/src/index.ts
  • exec/prepare-package/src/index.ts
  • fetching/tarball-fetcher/src/gitHostedTarballFetcher.ts
  • installing/deps-restorer/src/linkHoistedModules.ts
  • installing/commands/src/handleIgnoredBuilds.ts
  • installing/deps-restorer/src/index.ts
  • deps/graph-builder/src/lockfileToDepGraph.ts
  • fetching/git-fetcher/src/index.ts
  • building/policy/test/index.ts
  • installing/deps-resolver/src/resolveDependencies.ts
  • building/during-install/src/index.ts
  • exec/prepare-package/test/index.ts
  • building/commands/src/policy/approveBuilds.ts
  • building/after-install/src/index.ts
  • deps/graph-hasher/src/index.ts
  • building/policy/src/index.ts
  • installing/deps-installer/src/install/index.ts
  • installing/deps-installer/test/install/lifecycleScripts.ts
  • building/commands/test/build/index.ts
📚 Learning: 2026-05-26T21:01:06.666Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11966
File: .changeset/require-tarball-integrity.md:6-6
Timestamp: 2026-05-26T21:01:06.666Z
Learning: In pnpm lockfile-related release notes/docs (especially changeset markdown), preserve URL hostnames exactly as they appear in pnpm-lock.yaml tarball resolution entries—keep hosts like `codeload.github.com`, `bitbucket.org`, and `gitlab.com` in lowercase. Do not “correct” them to title-case/preserve brand capitalization (e.g., LanguageTool rules like `GITHUB` capitalization) because these are literal URL fragments, not platform brand names.

Applied to files:

  • .changeset/tough-allow-builds-identities.md
📚 Learning: 2026-06-05T13:47:05.929Z
Learnt from: vsumner
Repo: pnpm/pnpm PR: 12190
File: installing/deps-installer/test/install/frozenStore.ts:2-17
Timestamp: 2026-06-05T13:47:05.929Z
Learning: In the pnpm/pnpm repository, the shared Jest preset keeps `injectGlobals` at its default (`true`), so `test` and `expect` are available as Jest globals. Therefore, reviewers should not flag (or treat as TypeScript/compilation errors) missing `import { test, expect } from 'jest/globals'` when a test file uses `test`/`expect` without importing them. Importing from `jest/globals` may still be used for consistency with sibling files, but it is not required for execution in this repo unless a Jest preset is explicitly configured with `injectGlobals: false`.

Applied to files:

  • deps/graph-hasher/test/allowBuildIdentity.test.ts
  • building/policy/test/index.ts
  • exec/prepare-package/test/index.ts
  • installing/deps-installer/test/install/lifecycleScripts.ts
  • building/commands/test/build/index.ts
📚 Learning: 2026-05-20T19:40:55.051Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11774
File: pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs:0-0
Timestamp: 2026-05-20T19:40:55.051Z
Learning: In the pacquet Rust code, ensure the semver implementation uses the `node-semver` crate (not `nodejs-semver`). `node-semver`’s public API does not include a `satisfies_with_prerelease`-style method; prerelease-tolerant matching should be implemented inline by first calling `Range::satisfies`, and when it rejects a prerelease version, retry matching against a stripped `MAJOR.MINOR.PATCH` base of the prerelease version.

Applied to files:

  • pacquet/crates/package-manager/src/virtual_store_layout.rs
  • pacquet/crates/git-fetcher/src/tarball_fetcher.rs
  • pacquet/crates/git-fetcher/src/tarball_fetcher/tests.rs
  • pacquet/crates/package-manager/src/install_package_by_snapshot.rs
  • pacquet/crates/git-fetcher/src/fetcher.rs
  • pacquet/crates/git-fetcher/src/prepare_package.rs
  • pacquet/crates/git-fetcher/src/prepare_package/tests.rs
  • pacquet/crates/package-manager/src/build_modules/tests.rs
  • pacquet/crates/git-fetcher/src/fetcher/tests.rs
  • pacquet/crates/package-manager/src/build_modules.rs
📚 Learning: 2026-05-22T00:08:44.646Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11837
File: pacquet/crates/resolving-npm-resolver/src/pick_package.rs:33-51
Timestamp: 2026-05-22T00:08:44.646Z
Learning: In the pnpm/pnpm repo’s pacquet Rust crates, do not flag Unicode ellipsis characters (U+2026, `…`) in Rust doc comments (`///` / `/** */`) as a lint violation. The pacquet crate’s `dylint.toml` only enables `perfectionist::derive_ordering`, and the Dylint `unicode-ellipsis` rule is not enabled for this project—so `…` in doc comments is an intentional, repo-consistent style.

Applied to files:

  • pacquet/crates/package-manager/src/virtual_store_layout.rs
  • pacquet/crates/git-fetcher/src/tarball_fetcher.rs
  • pacquet/crates/git-fetcher/src/tarball_fetcher/tests.rs
  • pacquet/crates/package-manager/src/install_package_by_snapshot.rs
  • pacquet/crates/git-fetcher/src/fetcher.rs
  • pacquet/crates/git-fetcher/src/prepare_package.rs
  • pacquet/crates/git-fetcher/src/prepare_package/tests.rs
  • pacquet/crates/package-manager/src/build_modules/tests.rs
  • pacquet/crates/git-fetcher/src/fetcher/tests.rs
  • pacquet/crates/package-manager/src/build_modules.rs
📚 Learning: 2026-05-20T23:07:58.444Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11784
File: pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs:120-133
Timestamp: 2026-05-20T23:07:58.444Z
Learning: When reviewing code in this pacquet Rust port, follow the upstream pnpm compatibility rule: only match pnpm’s behavior exactly. Do not propose review changes that intentionally deviate from pnpm’s documented/observed behavior, even if pnpm appears buggy. If you identify a real bug in pnpm behavior, the review should prioritize fixing it upstream in pnpm first, and avoid implementing a pnpm-behavior workaround here unless the same fix has already landed upstream.

Applied to files:

  • pacquet/crates/package-manager/src/virtual_store_layout.rs
  • pacquet/crates/git-fetcher/src/tarball_fetcher.rs
  • pacquet/crates/git-fetcher/src/tarball_fetcher/tests.rs
  • pacquet/crates/package-manager/src/install_package_by_snapshot.rs
  • pacquet/crates/git-fetcher/src/fetcher.rs
  • pacquet/crates/git-fetcher/src/prepare_package.rs
  • pacquet/crates/git-fetcher/src/prepare_package/tests.rs
  • pacquet/crates/package-manager/src/build_modules/tests.rs
  • pacquet/crates/git-fetcher/src/fetcher/tests.rs
  • pacquet/crates/package-manager/src/build_modules.rs
📚 Learning: 2026-06-06T18:58:37.156Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 12243
File: pacquet/crates/package-manager/src/install_package_by_snapshot.rs:319-322
Timestamp: 2026-06-06T18:58:37.156Z
Learning: When reviewing Rust code, do not assume `matches!(expr, Pattern(_))` will move out of `expr` if `Pattern(_)` contains no by-value bindings. `matches!` desugars to a `match` that auto-borrows the scrutinee for discrimination, so even if `expr` is a non-`Copy` value behind a shared reference (e.g., `&T`), the macro should not move-out of the borrowed data purely due to `matches!`. Treat `matches!(&expr, Pattern(_))` as a readability/clarity improvement, not a correctness requirement. Only flag potential move-out-of-borrow risks when the pattern includes by-value bindings that would require moving the matched value.

Applied to files:

  • pacquet/crates/package-manager/src/virtual_store_layout.rs
  • pacquet/crates/git-fetcher/src/tarball_fetcher.rs
  • pacquet/crates/git-fetcher/src/tarball_fetcher/tests.rs
  • pacquet/crates/package-manager/src/install_package_by_snapshot.rs
  • pacquet/crates/git-fetcher/src/fetcher.rs
  • pacquet/crates/git-fetcher/src/prepare_package.rs
  • pacquet/crates/git-fetcher/src/prepare_package/tests.rs
  • pacquet/crates/package-manager/src/build_modules/tests.rs
  • pacquet/crates/git-fetcher/src/fetcher/tests.rs
  • pacquet/crates/package-manager/src/build_modules.rs
🔇 Additional comments (40)
.changeset/tough-allow-builds-identities.md (1)

1-17: LGTM!

building/commands/src/policy/getAutomaticallyIgnoredBuilds.ts (1)

3-3: LGTM!

Also applies to: 21-21, 38-43

installing/commands/src/handleIgnoredBuilds.ts (1)

2-2: LGTM!

Also applies to: 50-57

building/commands/test/build/index.ts (1)

50-50: LGTM!

Also applies to: 60-60, 333-333, 341-341, 387-387, 402-402

exec/prepare-package/src/index.ts (1)

22-50: LGTM!

exec/prepare-package/test/index.ts (2)

11-32: LGTM!


34-52: LGTM!

fetching/git-fetcher/src/index.ts (1)

50-51: LGTM!

fetching/tarball-fetcher/src/gitHostedTarballFetcher.ts (2)

83-88: LGTM!


129-135: LGTM!

installing/deps-installer/test/install/lifecycleScripts.ts (4)

3-3: LGTM!

Also applies to: 16-16, 20-20


314-333: LGTM!


335-378: LGTM!


380-408: LGTM!

pacquet/crates/git-fetcher/src/fetcher.rs (1)

1-154: LGTM!

pacquet/crates/git-fetcher/src/prepare_package.rs (1)

1-180: LGTM!

pacquet/crates/git-fetcher/src/prepare_package/tests.rs (1)

1-252: LGTM!

pacquet/crates/git-fetcher/src/tarball_fetcher/tests.rs (1)

1-14: LGTM!

pacquet/crates/package-manager/src/build_modules/tests.rs (1)

1-241: LGTM!

core/types/src/config.ts (1)

3-8: LGTM!

deps/graph-hasher/src/index.ts (1)

2-2: LGTM!

Also applies to: 163-170, 336-376

deps/graph-hasher/test/allowBuildIdentity.test.ts (1)

1-53: LGTM!

installing/deps-resolver/src/index.ts (1)

560-565: LGTM!

building/during-install/src/index.ts (1)

7-7: LGTM!

Also applies to: 95-95

installing/deps-installer/src/install/index.ts (1)

6-6: LGTM!

Also applies to: 61-61, 469-469, 487-491, 1096-1127

building/policy/src/index.ts (1)

63-65: Verify missing-context calls cannot bypass the trust gate.

Line 64 only blocks package-spec approvals when context.trustPackageIdentity === false; if context is omitted, the same rule still returns true. Please verify all production allowBuild call sites now pass createAllowBuildContext(...).

building/policy/test/index.ts (1)

2-3: LGTM!

Also applies to: 35-154

building/during-install/package.json (1)

37-37: LGTM!

building/during-install/tsconfig.json (1)

59-62: LGTM!

deps/graph-builder/package.json (1)

33-33: LGTM!

deps/graph-builder/src/lockfileToDepGraph.ts (1)

4-4: LGTM!

Also applies to: 245-248

installing/deps-resolver/src/resolveDependencies.ts (2)

260-260: LGTM!


1837-1837: LGTM!

deps/graph-builder/tsconfig.json (1)

12-14: LGTM!

installing/deps-installer/src/install/link.ts (2)

4-4: LGTM!


490-490: LGTM!

installing/deps-restorer/src/index.ts (2)

6-6: LGTM!


935-935: LGTM!

installing/deps-restorer/src/linkHoistedModules.ts (2)

4-4: LGTM!


139-139: LGTM!

Comment thread building/after-install/src/index.ts Outdated
Comment thread fetching/git-fetcher/src/index.ts Outdated
Comment thread pacquet/crates/package-manager/src/build_modules.rs Outdated
Comment thread pacquet/crates/package-manager/src/virtual_store_layout.rs Outdated
Comment thread pnpm-workspace.yaml
Comment thread building/policy/src/index.ts Outdated
@codecov-commenter

codecov-commenter commented Jun 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.88095% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.70%. Comparing base (a06adee) to head (13ef6c8).
⚠️ Report is 11 commits behind head on main.

Files with missing lines Patch % Lines
...le-verification/src/verify_lockfile_resolutions.rs 83.78% 12 Missing ⚠️
...acquet/crates/package-manager/src/build_modules.rs 94.87% 4 Missing ⚠️
...package-manager/src/install_package_by_snapshot.rs 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12294      +/-   ##
==========================================
+ Coverage   87.53%   87.70%   +0.17%     
==========================================
  Files         280      288       +8     
  Lines       33441    35177    +1736     
==========================================
+ Hits        29271    30851    +1580     
- Misses       4170     4326     +156     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@zkochan zkochan force-pushed the security/cand-pnpm-123-allowbuilds-deppath branch from ba4cc44 to 24fa6f8 Compare June 9, 2026 20:49
zkochan added 2 commits June 9, 2026 23:29
…provals

- Extract allowBuildKeyFromIgnoredBuild into @pnpm/building.policy (was
  triplicated) and reuse createAllowBuildContext in deps/graph-hasher.
- Reuse createGitHostedPkgId from @pnpm/resolving.git-resolver in the
  git fetcher instead of a local re-implementation.
- preparePackage always treats the fetched manifest as an untrusted
  package identity; drop its trustPackageIdentity and depPath options
  (mirrored in pacquet's PreparePackageOptions).
- Detect revoked build approvals for git/tarball artifacts in
  mutateModules; non-semver depPaths were skipped entirely.
- pacquet: normalize allowBuilds depPath keys with remove_suffix
  instead of a hand-rolled paren-truncating fallback.
- Drain the global log stream in the lifecycleScripts tests so reporter
  assertions only see their own installs' events instead of the
  buffered backlog of earlier reporterless installs.
@zkochan zkochan marked this pull request as ready for review June 9, 2026 21:40
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit b4366f4

The dlx git test still approved the shx build by package name, which the
allowBuilds hardening in this PR no longer accepts for git-hosted
artifacts. Every other test in the PR was already migrated to
depPath-keyed approvals; this one was missed.
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 7227a1d

Selectors and lockfile keys are now both normalized with
getPkgIdWithPatchHash, the same canonical form allowBuilds keys use.
The previous exact-or-removeSuffix comparison dropped the patch hash,
so the pkgIdWithPatchHash-shaped specs that
runUnignoredDependencyBuilds passes for patched artifacts matched
neither branch.
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 1dbfad8

Comment thread building/after-install/src/index.ts Outdated
The AllowBuild function now takes the package's DepPath instead of
caller-supplied name and version, which were duplicates of (or, in
preparePackage's case, manifest-claimed stand-ins for) information the
depPath already carries. The policy parses name and version out of the
depPath itself, so package-name rules can no longer be fed an identity
that disagrees with the artifact being gated.

- @pnpm/deps.path: add removePeersSuffix, the string-level primitive
  for normalizing user-written allowBuilds keys; getPkgIdWithPatchHash
  delegates to it.
- AllowBuildContext shrinks to { trustPackageIdentity }.
- preparePackage requires pkgResolutionId and synthesizes the depPath
  from it (the one deliberate brand cast, since that identity is not a
  lockfile key).
- pacquet: AllowBuildPolicy::check/check_with_context take the dep
  path; the git-fetcher closure shape becomes (dep_path, trust).
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12294
Testbedpnpr

⚠️ WARNING: No Threshold found!

Without a Threshold, no Alerts will ever be generated.

Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the --ci-only-thresholds flag.

Click to view all benchmark results
BenchmarkLatencymilliseconds (ms)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
5,088.83 ms
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
695.38 ms
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
678.47 ms
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
5,435.97 ms
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
779.24 ms
🐰 View full continuous benchmarking report in Bencher

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 10, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 1f896a9

matches() now compares pre-normalized pkgIdWithPatchHash values: the
user-written depPath spec is stripped of its peer suffix when the
selector is constructed, and the lockfile key when the candidate is
derived, so the matcher itself is plain equality. The suffixed key
still flows out of findPackages — its return value keys the lockfile
entries and virtual-store lookups.
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 10, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit af4e687

The fallback in _rebuild (findLinkedGvsDir) exists because the GVS
projection hash includes the allowBuilds verdict: an install that
deferred a build creates the projection under the not-built hash, and
a later rebuild running under the granting policy recomputes the built
hash, which points at a directory the install never created. The new
test proves the flow: it fails when the fallback is replaced with the
plain hash join.

Also type allowBuildKeyFromIgnoredBuild's parameter as DepPath —
ignoredBuilds is Set<DepPath>, so the brand flows in without casts and
the peer suffix is stripped with getPkgIdWithPatchHash.
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 10, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 3478af8

…ords

recordLockfileVerified (and pacquet's record_lockfile_verified) write
the verification cache record after a fresh resolve, but did so
without the resolutionShapeCheck flag, so the next offline no-op
install missed the cache and re-contacted the registry. Both
recorders now share the gate's withResolutionShapeCacheIdentity
helper; the freshly-resolved lockfile satisfies the shape invariant
by construction.

Also migrate the dlx cache e2e to approve the git-hosted shx by its
depPath, like the other dlx tests in this PR.
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 10, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 41f0b16

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 10, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit eca97f8

Comment thread resolving/git-resolver/src/createGitHostedPkgId.ts
…solvers

Two holes in the structural lockfile-shape pass:

- gitHosted was trusted as a strict `=== true`, so a tampered lockfile
  could set a non-boolean (e.g. 'true') or an explicit `false` on a
  git-host URL to make a git-hosted tarball look registry-shaped under a
  name@semver key. The check now treats any non-boolean flag as
  git-hosted and falls back to isGitHostedTarballUrl, so the verdict
  never depends on the flag alone. pacquet gates on the URL too
  (is_git_hosted_tarball_url made public).

- custom resolver protocols (type: 'custom:*') are a legitimate
  non-registry source under a semver key and were wrongly rejected with
  ERR_PNPM_RESOLUTION_SHAPE_MISMATCH. They are exempt: an unrecognized
  custom type can only be materialized by a project-configured custom
  fetcher and otherwise throws at fetch time, so the exemption cannot
  launder an artifact into a build. pacquet has no custom-resolution
  variant, so only the TS gate needs the carve-out.
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 10, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit f415118

Comment thread building/after-install/src/index.ts
Comment thread installing/deps-installer/src/install/verifyLockfileResolutions.ts
- isRegistryShapedResolution / is_registry_shaped_resolution now require a
  present tarball URL to be http(s): the npm verifier's tarball-URL binding
  skips non-http(s) schemes, so a file: (or other) tarball under a
  name@semver key would otherwise be trusted with no safety net. Absent /
  empty tarball stays registry-shaped (URL reconstructed from name+version).
- normalizeGitRepoForPkgResolutionId short-circuits when the repo already
  carries a URL scheme, so a fully-qualified ssh://user@host:port/path is no
  longer mangled into ssh://ssh://... by the scp-shorthand rewrite. pacquet's
  create_git_hosted_pkg_id has no scp regex, so it isn't affected.

Also add a regression test for the artifact-approval revocation path: a
tarball dep approved by depPath, then reinstalled with the approval removed,
must surface as an ignored build (covers the snapshot-name/version revocation
walk that reaches non-semver artifact depPaths).
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 10, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit f38b7bd

zkochan added 2 commits June 10, 2026 11:50
isGitHostedTarballUrl / is_git_hosted_tarball_url matched lowercase host
prefixes only, so a tampered lockfile could uppercase the host
(https://CODELOAD.GITHUB.COM/...) and set gitHosted: false to slip a
git-hosted tarball past the resolution-shape gate as a registry-trusted
identity under a name@semver key. Both helpers now lowercase the URL before
the scheme/host/tar.gz checks (detection only; the URL is never rewritten).

Also dedupe the lockfile.fs copy of the helper, which already depended on
@pnpm/lockfile.utils, so there's a single hardened implementation.
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 10, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 13ef6c8

Comment thread installing/deps-installer/src/install/index.ts
@zkochan zkochan merged commit bf1b731 into main Jun 10, 2026
25 checks passed
@zkochan zkochan deleted the security/cand-pnpm-123-allowbuilds-deppath branch June 10, 2026 10:05
zkochan added a commit that referenced this pull request Jun 10, 2026
Port of #12294 (commit bf1b731) to release/10.

Package-name entries in onlyBuiltDependencies (and allowBuilds) no longer
approve lifecycle scripts for artifacts whose identity a name cannot pin:
git, git-hosted tarball, direct tarball, and local directory dependencies.
To approve such an artifact explicitly, use its peer-suffix-free lockfile
depPath as the key — the GIT_DEP_PREPARE_NOT_ALLOWED hint, pnpm
ignored-builds, and pnpm approve-builds print exactly that key.

- AllowBuild policy functions identify packages by DepPath instead of
  caller-supplied name/version. Identity trust is derived from the depPath
  shape: a registry-style depPath (name@semver) is a trusted identity.
- The trust is sound because lockfile entries are structurally checked
  wherever they are materialized into fetchable resolutions
  (pkgSnapshotToResolution) and before rebuild runs scripts: a
  registry-style key backed by a git, directory, or git-hosted tarball
  resolution is rejected with ERR_PNPM_RESOLUTION_SHAPE_MISMATCH.
- preparePackage requires a pkgResolutionId and gates on the synthesized
  name@<resolution id> depPath; scp-style git URLs are normalized to
  ssh:// form in resolution ids and the git fetcher reuses
  createGitHostedPkgId from the resolver.
- isGitHostedTarballUrl matches case-insensitively and is shared from
  @pnpm/lockfile.utils; new removePeersSuffix() in @pnpm/dependency-path
  and allowBuildKeyFromIgnoredBuild() in @pnpm/builder.policy.
- pnpm rebuild and approve-builds accept depPath specs for selecting and
  approving artifact builds; installs rebuild ignored builds approved by
  depPath keys.
- shell-quote is overridden to 1.8.4 (GHSA-w7jw-789q-3m8p /
  CVE-2026-9277).

Differences from main: v10 has no lockfile resolution verifier, so the
structural pass lives in pkgSnapshotToResolution and the rebuild loop; the
AllowBuild policy keeps v10's boolean return; the revoked-approval
detection and global-virtual-store hash changes have no v10 counterpart.
@zkochan

zkochan commented Jun 10, 2026

Copy link
Copy Markdown
Member Author

🚢 v11.5.3

zkochan added a commit that referenced this pull request Jun 10, 2026
* fix(security): harden build-approval artifact identities on v10

Port of #12294 (commit bf1b731) to release/10.

Package-name entries in onlyBuiltDependencies (and allowBuilds) no longer
approve lifecycle scripts for artifacts whose identity a name cannot pin:
git, git-hosted tarball, direct tarball, and local directory dependencies.
To approve such an artifact explicitly, use its peer-suffix-free lockfile
depPath as the key — the GIT_DEP_PREPARE_NOT_ALLOWED hint, pnpm
ignored-builds, and pnpm approve-builds print exactly that key.

- AllowBuild policy functions identify packages by DepPath instead of
  caller-supplied name/version. Identity trust is derived from the depPath
  shape: a registry-style depPath (name@semver) is a trusted identity.
- The trust is sound because lockfile entries are structurally checked
  wherever they are materialized into fetchable resolutions
  (pkgSnapshotToResolution) and before rebuild runs scripts: a
  registry-style key backed by a git, directory, or git-hosted tarball
  resolution is rejected with ERR_PNPM_RESOLUTION_SHAPE_MISMATCH.
- preparePackage requires a pkgResolutionId and gates on the synthesized
  name@<resolution id> depPath; scp-style git URLs are normalized to
  ssh:// form in resolution ids and the git fetcher reuses
  createGitHostedPkgId from the resolver.
- isGitHostedTarballUrl matches case-insensitively and is shared from
  @pnpm/lockfile.utils; new removePeersSuffix() in @pnpm/dependency-path
  and allowBuildKeyFromIgnoredBuild() in @pnpm/builder.policy.
- pnpm rebuild and approve-builds accept depPath specs for selecting and
  approving artifact builds; installs rebuild ignored builds approved by
  depPath keys.
- shell-quote is overridden to 1.8.4 (GHSA-w7jw-789q-3m8p /
  CVE-2026-9277).

Differences from main: v10 has no lockfile resolution verifier, so the
structural pass lives in pkgSnapshotToResolution and the rebuild loop; the
AllowBuild policy keeps v10's boolean return; the revoked-approval
detection and global-virtual-store hash changes have no v10 counterpart.

* test: serve the direct-tarball fixture from an off-registry origin

The registry mock binds only localhost, so a hard-coded 127.0.0.1 URL is
refused in CI, and a tarball URL on the registry's own origin resolves as
a registry package, which defeats the direct-tarball identity the test
needs. An in-test HTTP server redirecting to the mock provides a separate
origin whose binding the test controls.

* test: approve the git-protocol licenses fixture by its depPath

A git-hosted artifact has an untrusted package identity, so the
ajv-keywords build has to be approved by its depPath. Pin the fixture to
the commit the depPath names.
KSXGitHub pushed a commit that referenced this pull request Jun 10, 2026
Integrate the 9 commits main gained (#12271, #12294, #12301, #12303,
#12305, #12312, #12315, #12316, and the release/version bumps).

Conflict resolution: all four conflicts (record_lockfile_verified,
build_modules, hoisted_dep_graph, install) were between this branch's
lint edits and main's feature changes — took main's authoritative
versions; lint compliance is re-derived by re-running clippy in the
follow-up commit.
zkochan added a commit to vsumner/pnpm that referenced this pull request Jun 11, 2026
… backstop

The AllowBuild signature changed to (depPath, context?) on main
(pnpm#12294); the frozen-store blocking predicate still passed
(name, version).
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