Skip to content

fix(pacquet): resolve package-manager bootstrap through trusted registries#12471

Merged
zkochan merged 1 commit into
mainfrom
fix/pacquet-pm-bootstrap-registries
Jun 17, 2026
Merged

fix(pacquet): resolve package-manager bootstrap through trusted registries#12471
zkochan merged 1 commit into
mainfrom
fix/pacquet-pm-bootstrap-registries

Conversation

@zkochan

@zkochan zkochan commented Jun 17, 2026

Copy link
Copy Markdown
Member

What

Ports the GHSA-j2hc-m6cf-6jm8 fix (pnpm/pnpm#12296) to pacquet, keeping the two stacks in sync per the parity rule in AGENTS.md.

Why

When pnpm auto-switches to the version requested by packageManager / devEngines.packageManager, the package-manager bytes (pnpm / @pnpm/exe) must be resolved through trusted registries only. The TypeScript CLI was fixed in #12296 (config.packageManagerRegistries / packageManagerNetworkConfig), but pacquet was never ported.

Pacquet's sync_package_manager_dependencies built its resolver from config.resolved_registries() — the project/workspace registries, which a malicious repository controls via the workspace .npmrc or a pnpm-workspace.yaml registries: block. That lets a repository point package-manager bootstrap resolution at an attacker-controlled registry.

Pacquet only resolves and writes integrity metadata into pnpm-lock.yaml here (it does not download/execute the bytes itself), so its standalone impact is lockfile poisoning rather than direct RCE — but the trust boundary must match pnpm regardless.

How

  • Add Config::package_manager_bootstrap (PackageManagerBootstrap), built in Config::current() from a trusted-only fold of the URL-scoped env, auth.ini, and user .npmrc sources. The project .npmrc and pnpm-workspace.yaml registries: are excluded. It reuses the existing registry/proxy/TLS/auth application logic so the bootstrap cascade stays identical to the project cascade minus the repository-controlled sources. PNPM_CONFIG_REGISTRY still overrides the bootstrap default because it is user-controlled (mirrors pnpm's env/CLI registry handling).
  • Add EnvInstallerContext::for_package_manager, used only by the package-manager bootstrap path (sync_package_manager_dependencies). Project configDependencies resolution keeps the project registries via EnvInstallerContext::new, matching the narrow scope of the upstream TypeScript fix.

Tests

New regression tests in config/src/tests.rs:

  • package_manager_bootstrap_ignores_project_npmrc_registry
  • package_manager_bootstrap_ignores_workspace_yaml_registries
  • package_manager_bootstrap_defaults_to_npm_registry
  • package_manager_bootstrap_honors_env_registry

Each asserts normal installs still follow the project/workspace/env registry while package-manager bootstrap stays on the trusted (or npm-default) registry.

Verified locally: cargo fmt --check, workspace cargo clippy/dylint (-D warnings), cargo doc -D warnings, and cargo test for pacquet-config (291) / pacquet-env-installer (12) / pacquet-cli cli_args (72) all pass.

No changeset: pacquet-only change.


Written by an agent (Claude Code, claude-opus-4-8).

Summary by CodeRabbit

  • Bug Fixes
    • Improved package-manager “bootstrap” registry/auth resolution to rely only on trusted user settings, preventing project or workspace registry settings from overriding bootstrap defaults.
    • Ensured PNPM_CONFIG_REGISTRY updates both the effective runtime registry and the bootstrap default registry.
    • Updated environment lockfile dependency syncing to initialize the resolver using the package-manager bootstrap networking/security/auth context.
  • Tests
    • Added regression tests covering trusted-vs-repository registry precedence, default registry fallback, and PNPM_CONFIG_REGISTRY override behavior.

Suggested squash commit message body

Port the GHSA-j2hc-m6cf-6jm8 fix (pnpm/pnpm#12296) to pacquet.

When pnpm auto-switches to the version requested by `packageManager` /
`devEngines.packageManager`, the bootstrap (`pnpm` / `@pnpm/exe`) must be
resolved through trusted registries only. Pacquet was resolving it through
`config.resolved_registries()`, which a malicious repository controls via
the workspace `.npmrc` or `pnpm-workspace.yaml` `registries:` block.

Add `Config::package_manager_bootstrap`, built in `Config::current()` from a
trusted-only fold of the URL-scoped env, `auth.ini`, and user `.npmrc`
sources (the project `.npmrc` is excluded), reusing the existing
registry/proxy/TLS/auth application logic. It defaults to the public npm
registry, and `PNPM_CONFIG_REGISTRY` still overrides the default because it
is user-controlled.

`EnvInstallerContext::for_package_manager` routes only the package-manager
bootstrap path (`sync_package_manager_dependencies`) through this trusted
config; project `configDependencies` resolution keeps the project
registries, matching the narrow scope of the upstream TypeScript fix.

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

Copy link
Copy Markdown

PR Summary by Qodo

Fix pacquet package-manager bootstrap to use trusted registries only
🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

Description

• Route pnpm auto-switch bootstrap resolution through trusted-only registry/network config.
• Exclude project/workspace registry overrides from package-manager bootstrap to prevent lockfile
 poisoning.
• Add regression tests covering trusted defaults and env override behavior.
Diagram

graph TD
C0["Config::current()"] --> C1["Project config"] --> N1["EnvInstallerContext::new"] --> R1["NpmResolver/HTTP"]
C0 --> C2["PackageManagerBootstrap"] --> N2["EnvInstallerContext::for_package_manager"] --> R1
A1["install_config_deps"] --> N1
A2["sync_package_manager_dependencies"] --> N2
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Hardcode npmjs.org for bootstrap
  • ➕ Very small change surface
  • ➕ Eliminates any registry override ambiguity
  • ➖ Breaks legitimate enterprise mirrors/proxies configured at user level
  • ➖ Diverges from pnpm’s behavior and parity goals
  • ➖ Would not honor trusted env/user .npmrc settings needed in CI
2. Parameterize resolved_registries() with a trust mode
  • ➕ Avoids introducing a new config struct
  • ➕ Centralizes registry selection behind one API
  • ➖ Still needs parallel trusted network/auth fields (proxy/TLS/auth headers), not just registries
  • ➖ Risk of accidental callsites using the wrong mode without a dedicated type

Recommendation: Keep the PR’s approach: a dedicated PackageManagerBootstrap type built from explicitly trusted sources, and a separate EnvInstallerContext constructor for the bootstrap path. This mirrors upstream pnpm’s split (packageManagerRegistries/packageManagerNetworkConfig), reduces the chance of accidental reuse of repository-controlled settings, and keeps normal installs unchanged while fixing the trust boundary for package-manager auto-switch.

Files changed (5) +236 / -9

Bug fix (2) +137 / -8
config_deps.rsRoute package-manager bootstrap resolution through trusted context +44/-8

Route package-manager bootstrap resolution through trusted context

• Introduces EnvInstallerContext::for_package_manager and refactors context construction into a shared build() helper. sync_package_manager_dependencies now uses the trusted bootstrap config (registries/proxy/TLS/auth) instead of project/workspace registries.

pacquet/crates/cli/src/config_deps.rs

lib.rsAdd trusted PackageManagerBootstrap config derived during Config::current() +93/-0

Add trusted PackageManagerBootstrap config derived during Config::current()

• Adds Config::package_manager_bootstrap and the PackageManagerBootstrap struct holding trusted-only registry and network/auth settings. Config::current() now folds only trusted sources (URL-scoped env, auth.ini, user .npmrc) to build bootstrap config, and applies PNPM_CONFIG_REGISTRY as a user-controlled override for the bootstrap default.

pacquet/crates/config/src/lib.rs

Refactor (1) +1 / -1
npmrc_auth.rsMake NpmrcAuth clonable to support trusted-source folding +1/-1

Make NpmrcAuth clonable to support trusted-source folding

• Derives Clone for NpmrcAuth so individual parsed sources can be reused both for full config folding and the trusted-only bootstrap fold.

pacquet/crates/config/src/npmrc_auth.rs

Tests (2) +98 / -0
tests.rsAdd regression tests for trusted package-manager bootstrap registries +97/-0

Add regression tests for trusted package-manager bootstrap registries

• Adds four tests ensuring package-manager bootstrap ignores project .npmrc and workspace registries, defaults to npmjs.org when no trusted registry is set, and honors PNPM_CONFIG_REGISTRY. Each test asserts normal install registry behavior remains repository-configurable while bootstrap stays trusted.

pacquet/crates/config/src/tests.rs

tests.rsUpdate test config constructor for new bootstrap field +1/-0

Update test config constructor for new bootstrap field

• Extends the test helper Config initializer to include package_manager_bootstrap with a default value to keep compilation and tests passing.

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

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

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

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Bootstrap reads repo cache 🐞 Bug ⛨ Security
Description
EnvInstallerContext::for_package_manager switches registries/auth/proxy/TLS to trusted sources but
still builds the NpmResolver using Config.offline/prefer_offline and Config.cache_dir, which
pnpm-workspace.yaml can set. A malicious repo can set preferOffline/offline and point cacheDir at a
repo path containing a crafted metadata mirror so package-manager bootstrap resolves pnpm/@pnpm/exe
from attacker-controlled metadata and writes poisoned resolution/integrity into pnpm-lock.yaml.
Code

pacquet/crates/cli/src/config_deps.rs[R139-154]

+    /// Context for resolving the package manager pnpm auto-switches to
+    /// (`pnpm` / `@pnpm/exe`). Resolves through TRUSTED registries and
+    /// network config only — never the repository-controlled project
+    /// `.npmrc` / `pnpm-workspace.yaml` — so a malicious workspace cannot
+    /// redirect the package-manager bytes. See GHSA-j2hc-m6cf-6jm8.
+    fn for_package_manager(config: &Config) -> Result<Self> {
+        let bootstrap = &config.package_manager_bootstrap;
+        Self::build(
+            config,
+            &bootstrap.proxy,
+            &bootstrap.tls,
+            &bootstrap.tls_by_uri,
+            bootstrap.resolved_registries(),
+            Arc::clone(&bootstrap.auth_headers),
+        )
+    }
Evidence
The bootstrap context still passes the full Config into the common builder, and the NpmResolver is
configured from Config.cache_dir/offline/prefer_offline. Those fields are applied from
pnpm-workspace.yaml (repo-controlled), and the resolver’s pick path explicitly reads the on-disk
mirror and can return it without a network fetch when offline/prefer_offline is set, enabling
repository-provided mirror data to steer package-manager bootstrap resolution.

pacquet/crates/cli/src/config_deps.rs[139-154]
pacquet/crates/cli/src/config_deps.rs[186-197]
pacquet/crates/config/src/workspace_yaml.rs[121-129]
pacquet/crates/config/src/workspace_yaml.rs[726-742]
pacquet/crates/config/src/workspace_yaml.rs[917-919]
pacquet/crates/resolving-npm-resolver/src/pick_package.rs[496-533]

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

## Issue description
`EnvInstallerContext::for_package_manager` uses trusted bootstrap registries/proxy/TLS/auth, but the resolver it builds still inherits `offline`, `prefer_offline`, and `cache_dir` from `Config`, which are repository-controlled via `pnpm-workspace.yaml`. Because the npm resolver reads the on-disk mirror first (and can return without any network fetch) when `offline`/`prefer_offline` is enabled, a malicious repo can supply crafted mirror metadata and poison `packageManagerDependencies` integrity/resolution written into `pnpm-lock.yaml`.
## Issue Context
- `pnpm-workspace.yaml` can set `offline`, `prefer_offline`, and `cache_dir`, and these are applied onto `Config`.
- The npm resolver consults the on-disk mirror when `offline` or `prefer_offline` is set, and in `offline` mode it will return the mirror result without a network request.
- The package-manager bootstrap path (`sync_package_manager_dependencies`) uses this resolver to populate `packageManagerDependencies` in the env lockfile.
## Fix Focus Areas
- pacquet/crates/cli/src/config_deps.rs[139-214]
- pacquet/crates/config/src/workspace_yaml.rs[121-129]
- pacquet/crates/config/src/workspace_yaml.rs[726-742]
- pacquet/crates/config/src/workspace_yaml.rs[917-919]
- pacquet/crates/resolving-npm-resolver/src/pick_package.rs[496-533]
## What to change
1. Ensure the package-manager bootstrap resolver does **not** accept repository-controlled cache/offline settings.
 - Option A (minimal, localized): In `EnvInstallerContext::for_package_manager`, override resolver config to a safe trusted posture (e.g. `offline=false`, `prefer_offline=false`, and use a non-workspace cache dir).
 - Option B (more complete/parity-friendly): Extend `PackageManagerBootstrap` to carry trusted-only `offline`, `prefer_offline`, and `cache_dir` (built from trusted sources only) and have `for_package_manager` use those fields.
2. Add a regression test demonstrating the attack:
 - A temp project with `pnpm-workspace.yaml` setting `preferOffline: true` and `cacheDir: ./.cache`.
 - Pre-populate the expected mirror path for `pnpm` under that cache dir with crafted metadata.
 - Assert bootstrap resolution incorrectly uses the mirror today; after the fix, it should fetch from the trusted registry (or at least ignore the repo cache).

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



Remediation recommended

2. Bootstrap skips global config 🐞 Bug ≡ Correctness
Description
Config::current builds package_manager_bootstrap before applying the trusted global config.yaml
layer, so any user-configured global registry/registries affect normal installs but are ignored for
package-manager bootstrap resolution. This can cause package-manager bootstrap to resolve via the
npm default (or other earlier source) even when the user configured a trusted mirror globally,
leading to surprising behavior or failures in restricted-network setups.
Code

pacquet/crates/config/src/lib.rs[R1859-1875]

+        // Package-manager bootstrap (auto-switching to the `packageManager`
+        // / `devEngines.packageManager` version) must resolve through
+        // TRUSTED sources only — the builtin default, user `.npmrc`,
+        // `auth.ini`, and URL-scoped env vars — never the project `.npmrc`
+        // or `pnpm-workspace.yaml`, which a malicious repository controls.
+        // Capture the trusted subset (everything except `project_source`)
+        // before the full fold consumes the sources. Mirrors pnpm's
+        // `packageManagerRegistries` / `packageManagerNetworkConfig`, built
+        // from `trustedConfig`
+        // ([`config/reader/src/index.ts`](https://github.com/pnpm/pnpm/blob/822beb5fa0/config/reader/src/index.ts#L328-L342)).
+        // See GHSA-j2hc-m6cf-6jm8.
+        let trusted_sources =
+            [env_scoped_source.clone(), auth_ini_source.clone(), user_source.clone()];
+
    // Fold high-priority-first: the first present source is the
    // base, each lower source fills the gaps it left
    // ([`NpmrcAuth::merge_under`]).
Evidence
The bootstrap config is built from only env-scoped auth + auth.ini + user .npmrc sources and
assigned before the later trusted global config.yaml layer is applied to the main Config. Since
global settings can set registry/registries, the main config will reflect them, but the
earlier-built bootstrap will not; the CLI then uses the bootstrap registries to resolve
package-manager dependencies.

pacquet/crates/config/src/lib.rs[1751-1759]
pacquet/crates/config/src/lib.rs[1859-1889]
pacquet/crates/config/src/lib.rs[1936-1943]
pacquet/crates/config/src/workspace_yaml.rs[810-822]
pacquet/crates/cli/src/config_deps.rs[139-153]

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

## Issue description
`package_manager_bootstrap` is constructed from trusted `.npmrc`/auth sources, but it is finalized **before** the trusted global config.yaml (`WorkspaceSettings::load_global`) is applied. As a result, a user’s global registry mirror (or global `registries:` routes) can steer normal installs but will not steer package-manager bootstrap resolution.
### Issue Context
- Bootstrap is created from a trusted subset of npmrc sources and stored in `self.package_manager_bootstrap`.
- The trusted global config.yaml layer is applied later to `self` (normal config), but never reflected into `package_manager_bootstrap`.
- Global settings can set `registry` and `registries`, and those are trusted/user-controlled (not repository-controlled).
### Fix Focus Areas
- pacquet/crates/config/src/lib.rs[1751-1943]
- pacquet/crates/config/src/lib.rs[1859-1889]
### Implementation sketch
- When building `package_manager_bootstrap`, also incorporate the trusted global settings layer (if present) **with the same precedence as normal config** but still excluding repository-controlled sources.
- Option A: Build a temporary `Config` for bootstrap (as today), then if `global_settings` is `Some`, apply it onto that temporary config (using the same save/restore for `workspace_dir` if needed), then extract `registry`, `registries`, `proxy`, `tls`, `tls_by_uri`, `auth_headers` into `PackageManagerBootstrap`.
- Option B: After applying `global_settings` to `self`, also update `self.package_manager_bootstrap.registry`/`.registries` from `global_settings`’ resolved values (keeping bootstrap’s proxy/tls/auth as-is), ensuring normalization matches `WorkspaceSettings::apply_to`.
- Keep the existing `PNPM_CONFIG_REGISTRY` override behavior for bootstrap.

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



Informational

3. Extra auth-state cloning 🐞 Bug ➹ Performance ⭐ New
Description
Config::current() clones multiple NpmrcAuth sources to build the trusted-only fold for
PackageManagerBootstrap, duplicating credential/TLS data in memory during config initialization.
This is likely minor, but it’s avoidable overhead on a frequently executed path and increases the
number of in-memory copies of sensitive strings (tokens/passwords/CA PEMs).
Code

pacquet/crates/config/src/lib.rs[R1857-1861]

+        // Capture the trusted sources (everything but `project_source`) for
+        // [`PackageManagerBootstrap`] before the fold below consumes them.
+        let trusted_sources =
+            [env_scoped_source.clone(), auth_ini_source.clone(), user_source.clone()];
+
Evidence
The PR introduces cloning of the parsed auth sources to preserve them for a second fold, which
required adding Clone to NpmrcAuth. NpmrcAuth stores credential maps and TLS/CA data, so
cloning it duplicates potentially sizeable and sensitive data structures.

pacquet/crates/config/src/lib.rs[1857-1861]
pacquet/crates/config/src/lib.rs[1872-1878]
pacquet/crates/config/src/npmrc_auth.rs[45-60]

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

### Issue description
`Config::current()` builds `trusted_sources` by cloning `Option<NpmrcAuth>` values so it can fold them twice (full config + bootstrap). Since `NpmrcAuth` contains credential maps and TLS/CA material, this duplicates potentially large structures and sensitive strings in memory during config loading.

### Issue Context
This happens every time config is resolved (CLI startup / install path), so even small overhead can add up. It’s also generally preferable to minimize the number of in-memory copies of auth material.

### Fix Focus Areas
- pacquet/crates/config/src/lib.rs[1857-1878]
- pacquet/crates/config/src/npmrc_auth.rs[45-60]

### Suggested approach
Refactor to avoid cloning whole `NpmrcAuth` instances. Options:
1. Implement a `merge_under_ref(&mut self, lower: &NpmrcAuth)` (or similar) that only clones fields when needed, allowing reuse of the parsed sources for both folds without cloning entire objects.
2. Alternatively, build the bootstrap config directly from the individual trusted sources without retaining a cloned array (e.g., fold into a `trusted_auth` accumulator as you parse, using borrowed data or selective cloning).

Keep behavior identical (same precedence order, same rescoping) while reducing full-structure clones.

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


Grey Divider

Previous review results

Review updated until commit 14dde1d

Results up to commit 5ad2b2d


🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (0)


Action required
1. Bootstrap reads repo cache 🐞 Bug ⛨ Security ⭐ New
Description
EnvInstallerContext::for_package_manager switches registries/auth/proxy/TLS to trusted sources but
still builds the NpmResolver using Config.offline/prefer_offline and Config.cache_dir, which
pnpm-workspace.yaml can set. A malicious repo can set preferOffline/offline and point cacheDir at a
repo path containing a crafted metadata mirror so package-manager bootstrap resolves pnpm/@pnpm/exe
from attacker-controlled metadata and writes poisoned resolution/integrity into pnpm-lock.yaml.
Code

pacquet/crates/cli/src/config_deps.rs[R139-154]

+    /// Context for resolving the package manager pnpm auto-switches to
+    /// (`pnpm` / `@pnpm/exe`). Resolves through TRUSTED registries and
+    /// network config only — never the repository-controlled project
+    /// `.npmrc` / `pnpm-workspace.yaml` — so a malicious workspace cannot
+    /// redirect the package-manager bytes. See GHSA-j2hc-m6cf-6jm8.
+    fn for_package_manager(config: &Config) -> Result<Self> {
+        let bootstrap = &config.package_manager_bootstrap;
+        Self::build(
+            config,
+            &bootstrap.proxy,
+            &bootstrap.tls,
+            &bootstrap.tls_by_uri,
+            bootstrap.resolved_registries(),
+            Arc::clone(&bootstrap.auth_headers),
+        )
+    }
Evidence
The bootstrap context still passes the full Config into the common builder, and the NpmResolver is
configured from Config.cache_dir/offline/prefer_offline. Those fields are applied from
pnpm-workspace.yaml (repo-controlled), and the resolver’s pick path explicitly reads the on-disk
mirror and can return it without a network fetch when offline/prefer_offline is set, enabling
repository-provided mirror data to steer package-manager bootstrap resolution.

pacquet/crates/cli/src/config_deps.rs[139-154]
pacquet/crates/cli/src/config_deps.rs[186-197]
pacquet/crates/config/src/workspace_yaml.rs[121-129]
pacquet/crates/config/src/workspace_yaml.rs[726-742]
pacquet/crates/config/src/workspace_yaml.rs[917-919]
pacquet/crates/resolving-npm-resolver/src/pick_package.rs[496-533]

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

## Issue description
`EnvInstallerContext::for_package_manager` uses trusted bootstrap registries/proxy/TLS/auth, but the resolver it builds still inherits `offline`, `prefer_offline`, and `cache_dir` from `Config`, which are repository-controlled via `pnpm-workspace.yaml`. Because the npm resolver reads the on-disk mirror first (and can return without any network fetch) when `offline`/`prefer_offline` is enabled, a malicious repo can supply crafted mirror metadata and poison `packageManagerDependencies` integrity/resolution written into `pnpm-lock.yaml`.

## Issue Context
- `pnpm-workspace.yaml` can set `offline`, `prefer_offline`, and `cache_dir`, and these are applied onto `Config`.
- The npm resolver consults the on-disk mirror when `offline` or `prefer_offline` is set, and in `offline` mode it will return the mirror result without a network request.
- The package-manager bootstrap path (`sync_package_manager_dependencies`) uses this resolver to populate `packageManagerDependencies` in the env lockfile.

## Fix Focus Areas
- pacquet/crates/cli/src/config_deps.rs[139-214]
- pacquet/crates/config/src/workspace_yaml.rs[121-129]
- pacquet/crates/config/src/workspace_yaml.rs[726-742]
- pacquet/crates/config/src/workspace_yaml.rs[917-919]
- pacquet/crates/resolving-npm-resolver/src/pick_package.rs[496-533]

## What to change
1. Ensure the package-manager bootstrap resolver does **not** accept repository-controlled cache/offline settings.
  - Option A (minimal, localized): In `EnvInstallerContext::for_package_manager`, override resolver config to a safe trusted posture (e.g. `offline=false`, `prefer_offline=false`, and use a non-workspace cache dir).
  - Option B (more complete/parity-friendly): Extend `PackageManagerBootstrap` to carry trusted-only `offline`, `prefer_offline`, and `cache_dir` (built from trusted sources only) and have `for_package_manager` use those fields.
2. Add a regression test demonstrating the attack:
  - A temp project with `pnpm-workspace.yaml` setting `preferOffline: true` and `cacheDir: ./.cache`.
  - Pre-populate the expected mirror path for `pnpm` under that cache dir with crafted metadata.
  - Assert bootstrap resolution incorrectly uses the mirror today; after the fix, it should fetch from the trusted registry (or at least ignore the repo cache).

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



Remediation recommended
2. Bootstrap skips global config 🐞 Bug ≡ Correctness
Description
Config::current builds package_manager_bootstrap before applying the trusted global config.yaml
layer, so any user-configured global registry/registries affect normal installs but are ignored for
package-manager bootstrap resolution. This can cause package-manager bootstrap to resolve via the
npm default (or other earlier source) even when the user configured a trusted mirror globally,
leading to surprising behavior or failures in restricted-network setups.
Code

pacquet/crates/config/src/lib.rs[R1859-1875]

+        // Package-manager bootstrap (auto-switching to the `packageManager`
+        // / `devEngines.packageManager` version) must resolve through
+        // TRUSTED sources only — the builtin default, user `.npmrc`,
+        // `auth.ini`, and URL-scoped env vars — never the project `.npmrc`
+        // or `pnpm-workspace.yaml`, which a malicious repository controls.
+        // Capture the trusted subset (everything except `project_source`)
+        // before the full fold consumes the sources. Mirrors pnpm's
+        // `packageManagerRegistries` / `packageManagerNetworkConfig`, built
+        // from `trustedConfig`
+        // ([`config/reader/src/index.ts`](https://github.com/pnpm/pnpm/blob/822beb5fa0/config/reader/src/index.ts#L328-L342)).
+        // See GHSA-j2hc-m6cf-6jm8.
+        let trusted_sources =
+            [env_scoped_source.clone(), auth_ini_source.clone(), user_source.clone()];
+
     // Fold high-priority-first: the first present source is the
     // base, each lower source fills the gaps it left
     // ([`NpmrcAuth::merge_under`]).
Evidence
The bootstrap config is built from only env-scoped auth + auth.ini + user .npmrc sources and
assigned before the later trusted global config.yaml layer is applied to the main Config. Since
global settings can set registry/registries, the main config will reflect them, but the
earlier-built bootstrap will not; the CLI then uses the bootstrap registries to resolve
package-manager dependencies.

pacquet/crates/config/src/lib.rs[1751-1759]
pacquet/crates/config/src/lib.rs[1859-1889]
pacquet/crates/config/src/lib.rs[1936-1943]
pacquet/crates/config/src/workspace_yaml.rs[810-822]
pacquet/crates/cli/src/config_deps.rs[139-153]

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

## Issue description
`package_manager_bootstrap` is constructed from trusted `.npmrc`/auth sources, but it is finalized **before** the trusted global config.yaml (`WorkspaceSettings::load_global`) is applied. As a result, a user’s global registry mirror (or global `registries:` routes) can steer normal installs but will not steer package-manager bootstrap resolution.
### Issue Context
- Bootstrap is created from a trusted subset of npmrc sources and stored in `self.package_manager_bootstrap`.
- The trusted global config.yaml layer is applied later to `self` (normal config), but never reflected into `package_manager_bootstrap`.
- Global settings can set `registry` and `registries`, and those are trusted/user-controlled (not repository-controlled).
### Fix Focus Areas
- pacquet/crates/config/src/lib.rs[1751-1943]
- pacquet/crates/config/src/lib.rs[1859-1889]
### Implementation sketch
- When building `package_manager_bootstrap`, also incorporate the trusted global settings layer (if present) **with the same precedence as normal config** but still excluding repository-controlled sources.
- Option A: Build a temporary `Config` for bootstrap (as today), then if `global_settings` is `Some`, apply it onto that temporary config (using the same save/restore for `workspace_dir` if needed), then extract `registry`, `registries`, `proxy`, `tls`, `tls_by_uri`, `auth_headers` into `PackageManagerBootstrap`.
- Option B: After applying `global_settings` to `self`, also update `self.package_manager_bootstrap.registry`/`.registries` from `global_settings`’ resolved values (keeping bootstrap’s proxy/tls/auth as-is), ensuring normalization matches `WorkspaceSettings::apply_to`.
- Keep the existing `PNPM_CONFIG_REGISTRY` override behavior for bootstrap.

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


Results up to commit d502bbb


🐞 Bugs (1) 📘 Rule violations (0) 📜 Skill insights (0)


Remediation recommended
1. Bootstrap skips global config 🐞 Bug ≡ Correctness
Description
Config::current builds package_manager_bootstrap before applying the trusted global config.yaml
layer, so any user-configured global registry/registries affect normal installs but are ignored for
package-manager bootstrap resolution. This can cause package-manager bootstrap to resolve via the
npm default (or other earlier source) even when the user configured a trusted mirror globally,
leading to surprising behavior or failures in restricted-network setups.
Code

pacquet/crates/config/src/lib.rs[R1859-1875]

+        // Package-manager bootstrap (auto-switching to the `packageManager`
+        // / `devEngines.packageManager` version) must resolve through
+        // TRUSTED sources only — the builtin default, user `.npmrc`,
+        // `auth.ini`, and URL-scoped env vars — never the project `.npmrc`
+        // or `pnpm-workspace.yaml`, which a malicious repository controls.
+        // Capture the trusted subset (everything except `project_source`)
+        // before the full fold consumes the sources. Mirrors pnpm's
+        // `packageManagerRegistries` / `packageManagerNetworkConfig`, built
+        // from `trustedConfig`
+        // ([`config/reader/src/index.ts`](https://github.com/pnpm/pnpm/blob/822beb5fa0/config/reader/src/index.ts#L328-L342)).
+        // See GHSA-j2hc-m6cf-6jm8.
+        let trusted_sources =
+            [env_scoped_source.clone(), auth_ini_source.clone(), user_source.clone()];
+
      // Fold high-priority-first: the first present source is the
      // base, each lower source fills the gaps it left
      // ([`NpmrcAuth::merge_under`]).
Evidence
The bootstrap config is built from only env-scoped auth + auth.ini + user .npmrc sources and
assigned before the later trusted global config.yaml layer is applied to the main Config. Since
global settings can set registry/registries, the main config will reflect them, but the
earlier-built bootstrap will not; the CLI then uses the bootstrap registries to resolve
package-manager dependencies.

pacquet/crates/config/src/lib.rs[1751-1759]
pacquet/crates/config/src/lib.rs[1859-1889]
pacquet/crates/config/src/lib.rs[1936-1943]
pacquet/crates/config/src/workspace_yaml.rs[810-822]
pacquet/crates/cli/src/config_deps.rs[139-153]

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

## Issue description
`package_manager_bootstrap` is constructed from trusted `.npmrc`/auth sources, but it is finalized **before** the trusted global config.yaml (`WorkspaceSettings::load_global`) is applied. As a result, a user’s global registry mirror (or global `registries:` routes) can steer normal installs but will not steer package-manager bootstrap resolution.
### Issue Context
- Bootstrap is created from a trusted subset of npmrc sources and stored in `self.package_manager_bootstrap`.
- The trusted global config.yaml layer is applied later to `self` (normal config), but never reflected into `package_manager_bootstrap`.
- Global settings can set `registry` and `registries`, and those are trusted/user-controlled (not repository-controlled).
### Fix Focus Areas
- pacquet/crates/config/src/lib.rs[1751-1943]
- pacquet/crates/config/src/lib.rs[1859-1889]
### Implementation sketch
- When building `package_manager_bootstrap`, also incorporate the trusted global settings layer (if present) **with the same precedence as normal config** but still excluding repository-controlled sources.
- Option A: Build a temporary `Config` for bootstrap (as today), then if `global_settings` is `Some`, apply it onto that temporary config (using the same save/restore for `workspace_dir` if needed), then extract `registry`, `registries`, `proxy`, `tls`, `tls_by_uri`, `auth_headers` into `PackageManagerBootstrap`.
- Option B: After applying `global_settings` to `self`, also update `self.package_manager_bootstrap.registry`/`.registries` from `global_settings`’ resolved values (keeping bootstrap’s proxy/tls/auth as-is), ensuring normalization matches `WorkspaceSettings::apply_to`.
- Keep the existing `PNPM_CONFIG_REGISTRY` override behavior for bootstrap.

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


Results up to commit a147d39


🐞 Bugs (1) 📘 Rule violations (0) 📜 Skill insights (0)


Remediation recommended
1. Bootstrap skips global config 🐞 Bug ≡ Correctness
Description
Config::current builds package_manager_bootstrap before applying the trusted global config.yaml
layer, so any user-configured global registry/registries affect normal installs but are ignored for
package-manager bootstrap resolution. This can cause package-manager bootstrap to resolve via the
npm default (or other earlier source) even when the user configured a trusted mirror globally,
leading to surprising behavior or failures in restricted-network setups.
Code

pacquet/crates/config/src/lib.rs[R1859-1875]

+        // Package-manager bootstrap (auto-switching to the `packageManager`
+        // / `devEngines.packageManager` version) must resolve through
+        // TRUSTED sources only — the builtin default, user `.npmrc`,
+        // `auth.ini`, and URL-scoped env vars — never the project `.npmrc`
+        // or `pnpm-workspace.yaml`, which a malicious repository controls.
+        // Capture the trusted subset (everything except `project_source`)
+        // before the full fold consumes the sources. Mirrors pnpm's
+        // `packageManagerRegistries` / `packageManagerNetworkConfig`, built
+        // from `trustedConfig`
+        // ([`config/reader/src/index.ts`](https://github.com/pnpm/pnpm/blob/822beb5fa0/config/reader/src/index.ts#L328-L342)).
+        // See GHSA-j2hc-m6cf-6jm8.
+        let trusted_sources =
+            [env_scoped_source.clone(), auth_ini_source.clone(), user_source.clone()];
+
       // Fold high-priority-first: the first present source is the
       // base, each lower source fills the gaps it left
       // ([`NpmrcAuth::merge_under`]).
Evidence
The bootstrap config is built from only env-scoped auth + auth.ini + user .npmrc sources and
assigned before the later trusted global config.yaml layer is applied to the main Config. Since
global settings can set registry/registries, the main config will reflect them, but the
earlier-built bootstrap will not; the CLI then uses the bootstrap registries to resolve
package-manager dependencies.

pacquet/crates/config/src/lib.rs[1751-1759]
pacquet/crates/config/src/lib.rs[1859-1889]
pacquet/crates/config/src/lib.rs[1936-1943]
pacquet/crates/config/src/workspace_yaml.rs[810-822]
pacquet/crates/cli/src/config_deps.rs[139-153]

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

## Issue description
`package_manager_bootstrap` is constructed from trusted `.npmrc`/auth sources, but it is finalized **before** the trusted global config.yaml (`WorkspaceSettings::load_global`) is applied. As a result, a user’s global registry mirror (or global `registries:` routes) can steer normal installs but will not steer package-manager bootstrap resolution.
### Issue Context
- Bootstrap is created from a trusted subset of npmrc sources and stored in `self.package_manager_bootstrap`.
- The trusted global config.yaml layer is applied later to `self` (normal config), but never reflected into `package_manager_bootstrap`.
- Global settings can set `registry` and `registries`, and those are trusted/user-controlled (not repository-controlled).
### Fix Focus Areas
- pacquet/crates/config/src/lib.rs[1751-1943]
- pacquet/crates/config/src/lib.rs[1859-1889]
### Implementation sketch
- When building `package_manager_bootstrap`, also incorporate the trusted global settings layer (if present) **with the same precedence as normal config** but still excluding repository-controlled sources.
- Option A: Build a temporary `Config` for bootstrap (as today), then if `global_settings` is `Some`, apply it onto that temporary config (using the same save/restore for `workspace_dir` if needed), then extract `registry`, `registries`, `proxy`, `tls`, `tls_by_uri`, `auth_headers` into `PackageManagerBootstrap`.
- Option B: After applying `global_settings` to `self`, also update `self.package_manager_bootstrap.registry`/`.registries` from `global_settings`’ resolved values (keeping bootstrap’s proxy/tls/auth as-is), ensuring normalization matches `WorkspaceSettings::apply_to`.
- Keep the existing `PNPM_CONFIG_REGISTRY` override behavior for bootstrap.

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


Results up to commit 8f17eaf


🐞 Bugs (1) 📘 Rule violations (0) 📜 Skill insights (0)


Remediation recommended
1. Bootstrap skips global config 🐞 Bug ≡ Correctness
Description
Config::current builds package_manager_bootstrap before applying the trusted global config.yaml
layer, so any user-configured global registry/registries affect normal installs but are ignored for
package-manager bootstrap resolution. This can cause package-manager bootstrap to resolve via the
npm default (or other earlier source) even when the user configured a trusted mirror globally,
leading to surprising behavior or failures in restricted-network setups.
Code

pacquet/crates/config/src/lib.rs[R1859-1875]

+        // Package-manager bootstrap (auto-switching to the `packageManager`
+        // / `devEngines.packageManager` version) must resolve through
+        // TRUSTED sources only — the builtin default, user `.npmrc`,
+        // `auth.ini`, and URL-scoped env vars — never the project `.npmrc`
+        // or `pnpm-workspace.yaml`, which a malicious repository controls.
+        // Capture the trusted subset (everything except `project_source`)
+        // before the full fold consumes the sources. Mirrors pnpm's
+        // `packageManagerRegistries` / `packageManagerNetworkConfig`, built
+        // from `trustedConfig`
+        // ([`config/reader/src/index.ts`](https://github.com/pnpm/pnpm/blob/822beb5fa0/config/reader/src/index.ts#L328-L342)).
+        // See GHSA-j2hc-m6cf-6jm8.
+        let trusted_sources =
+            [env_scoped_source.clone(), auth_ini_source.clone(), user_source.clone()];
+
        // Fold high-priority-first: the first present source is the
        // base, each lower source fills the gaps it left
        // ([`NpmrcAuth::merge_under`]).
Evidence
The bootstrap config is built from only env-scoped auth + auth.ini + user .npmrc sources and
assigned before the later trusted global config.yaml layer is applied to the main Config. Since
global settings can set registry/registries, the main config will reflect them, but the
earlier-built bootstrap will not; the CLI then uses the bootstrap registries to resolve
package-manager dependencies.

pacquet/crates/config/src/lib.rs[1751-1759]
pacquet/crates/config/src/lib.rs[1859-1889]
pacquet/crates/config/src/lib.rs[1936-1943]
pacquet/crates/config/src/workspace_yaml.rs[810-822]
pacquet/crates/cli/src/config_deps.rs[139-153]

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

### Issue description
`package_manager_bootstrap` is constructed from trusted `.npmrc`/auth sources, but it is finalized **before** the trusted global config.yaml (`WorkspaceSettings::load_global`) is applied. As a result, a user’s global registry mirror (or global `registries:` routes) can steer normal installs but will not steer package-manager bootstrap resolution.

### Issue Context
- Bootstrap is created from a trusted subset of npmrc sources and stored in `self.package_manager_bootstrap`.
- The trusted global config.yaml layer is applied later to `self` (normal config), but never reflected into `package_manager_bootstrap`.
- Global settings can set `registry` and `registries`, and those are trusted/user-controlled (not repository-controlled).

### Fix Focus Areas
- pacquet/crates/config/src/lib.rs[1751-1943]
- pacquet/crates/config/src/lib.rs[1859-1889]

### Implementation sketch
- When building `package_manager_bootstrap`, also incorporate the trusted global settings layer (if present) **with the same precedence as normal config** but still excluding repository-controlled sources.
 - Option A: Build a temporary `Config` for bootstrap (as today), then if `global_settings` is `Some`, apply it onto that temporary config (using the same save/restore for `workspace_dir` if needed), then extract `registry`, `registries`, `proxy`, `tls`, `tls_by_uri`, `auth_headers` into `PackageManagerBootstrap`.
 - Option B: After applying `global_settings` to `self`, also update `self.package_manager_bootstrap.registry`/`.registries` from `global_settings`’ resolved values (keeping bootstrap’s proxy/tls/auth as-is), ensuring normalization matches `WorkspaceSettings::apply_to`.
- Keep the existing `PNPM_CONFIG_REGISTRY` override behavior for bootstrap.

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


Qodo Logo

Comment thread pacquet/crates/config/src/lib.rs Outdated
Comment on lines 1859 to 1875
// Package-manager bootstrap (auto-switching to the `packageManager`
// / `devEngines.packageManager` version) must resolve through
// TRUSTED sources only — the builtin default, user `.npmrc`,
// `auth.ini`, and URL-scoped env vars — never the project `.npmrc`
// or `pnpm-workspace.yaml`, which a malicious repository controls.
// Capture the trusted subset (everything except `project_source`)
// before the full fold consumes the sources. Mirrors pnpm's
// `packageManagerRegistries` / `packageManagerNetworkConfig`, built
// from `trustedConfig`
// ([`config/reader/src/index.ts`](https://github.com/pnpm/pnpm/blob/822beb5fa0/config/reader/src/index.ts#L328-L342)).
// See GHSA-j2hc-m6cf-6jm8.
let trusted_sources =
[env_scoped_source.clone(), auth_ini_source.clone(), user_source.clone()];

// Fold high-priority-first: the first present source is the
// base, each lower source fills the gaps it left
// ([`NpmrcAuth::merge_under`]).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. Bootstrap skips global config 🐞 Bug ≡ Correctness

Config::current builds package_manager_bootstrap before applying the trusted global config.yaml
layer, so any user-configured global registry/registries affect normal installs but are ignored for
package-manager bootstrap resolution. This can cause package-manager bootstrap to resolve via the
npm default (or other earlier source) even when the user configured a trusted mirror globally,
leading to surprising behavior or failures in restricted-network setups.
Agent Prompt
### Issue description
`package_manager_bootstrap` is constructed from trusted `.npmrc`/auth sources, but it is finalized **before** the trusted global config.yaml (`WorkspaceSettings::load_global`) is applied. As a result, a user’s global registry mirror (or global `registries:` routes) can steer normal installs but will not steer package-manager bootstrap resolution.

### Issue Context
- Bootstrap is created from a trusted subset of npmrc sources and stored in `self.package_manager_bootstrap`.
- The trusted global config.yaml layer is applied later to `self` (normal config), but never reflected into `package_manager_bootstrap`.
- Global settings can set `registry` and `registries`, and those are trusted/user-controlled (not repository-controlled).

### Fix Focus Areas
- pacquet/crates/config/src/lib.rs[1751-1943]
- pacquet/crates/config/src/lib.rs[1859-1889]

### Implementation sketch
- When building `package_manager_bootstrap`, also incorporate the trusted global settings layer (if present) **with the same precedence as normal config** but still excluding repository-controlled sources.
  - Option A: Build a temporary `Config` for bootstrap (as today), then if `global_settings` is `Some`, apply it onto that temporary config (using the same save/restore for `workspace_dir` if needed), then extract `registry`, `registries`, `proxy`, `tls`, `tls_by_uri`, `auth_headers` into `PackageManagerBootstrap`.
  - Option B: After applying `global_settings` to `self`, also update `self.package_manager_bootstrap.registry`/`.registries` from `global_settings`’ resolved values (keeping bootstrap’s proxy/tls/auth as-is), ensuring normalization matches `WorkspaceSettings::apply_to`.
- Keep the existing `PNPM_CONFIG_REGISTRY` override behavior for bootstrap.

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is intentional, for parity with the TypeScript implementation. Upstream computes packageManagerRegistries from npmrcResult.trustedConfig (the .npmrc layers only); the global config.yaml registry is applied to the normal config but is never folded into the package-manager bootstrap registries either (config/reader/src/index.ts builds packageManagerRegistries from pickIniConfig(npmrcResult.trustedConfig), not from the global workspace-manifest config). pacquet mirrors that: the trusted bootstrap set is the builtin default + user .npmrc + auth.ini + URL-scoped env, and PNPM_CONFIG_REGISTRY overrides the default.

Per the pacquet cardinal rule (match pnpm exactly; don't "fix" pnpm behavior unless the same fix lands upstream first), I'm leaving this aligned with pnpm rather than adding global config.yaml to the bootstrap set. If we want global config to steer package-manager bootstrap, that should be a coordinated change in both stacks.


Written by an agent (Claude Code, claude-opus-4-8).

@coderabbitai

coderabbitai Bot commented Jun 17, 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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ff8b138e-613a-4614-aec8-9b61850ac524

📥 Commits

Reviewing files that changed from the base of the PR and between 5ad2b2d and 14dde1d.

📒 Files selected for processing (5)
  • pacquet/crates/cli/src/config_deps.rs
  • pacquet/crates/config/src/lib.rs
  • pacquet/crates/config/src/npmrc_auth.rs
  • pacquet/crates/config/src/tests.rs
  • pacquet/crates/package-manager/src/install_package_from_registry/tests.rs
✅ Files skipped from review due to trivial changes (1)
  • pacquet/crates/package-manager/src/install_package_from_registry/tests.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • pacquet/crates/config/src/npmrc_auth.rs
  • pacquet/crates/config/src/lib.rs
  • pacquet/crates/cli/src/config_deps.rs

📝 Walkthrough

Walkthrough

Introduces PackageManagerBootstrap, a new Config field derived exclusively from trusted sources (user .npmrc, auth.ini, env vars), excluding project .npmrc and pnpm-workspace.yaml. EnvInstallerContext gains a for_package_manager constructor that sources its network/registry settings from this bootstrap, and sync_package_manager_dependencies is updated to use it when resolving pnpm/@pnpm/exe dependencies.

Changes

Trusted-source bootstrap for package manager resolution

Layer / File(s) Summary
PackageManagerBootstrap struct and NpmrcAuth Clone
pacquet/crates/config/src/lib.rs, pacquet/crates/config/src/npmrc_auth.rs
Defines PackageManagerBootstrap with registry, registries, proxy, tls, tls_by_uri, and auth_headers fields plus resolved_registries() method; adds Clone to NpmrcAuth to enable auth cloning; adds package_manager_bootstrap field to Config.
Bootstrap derivation inside Config::current
pacquet/crates/config/src/lib.rs
During Config::current, collects a trusted_sources list (env-scoped auth, auth.ini, user .npmrc—excluding project .npmrc and workspace yaml); calls build_package_manager_bootstrap to apply registry/proxy/TLS/auth pipeline to a fresh default Config; syncs bootstrap.registry when PNPM_CONFIG_REGISTRY overrides the main registry.
EnvInstallerContext refactoring and for_package_manager constructor
pacquet/crates/cli/src/config_deps.rs
Refactors EnvInstallerContext constructors: new(config) and the new for_package_manager(config) both delegate to a shared build(...) helper; for_package_manager sources proxy/TLS/registries/auth from config.package_manager_bootstrap while new uses project-level fields; sync_package_manager_dependencies is updated to call for_package_manager for computing package-manager dependency integrities.
Regression tests and test fixture update
pacquet/crates/config/src/tests.rs, pacquet/crates/package-manager/src/install_package_from_registry/tests.rs
Five regression tests assert bootstrap isolation from project/workspace registries, correct fallback to npm default registry, and proper PNPM_CONFIG_REGISTRY propagation to both normal and bootstrap registries; package-manager test helper initializes package_manager_bootstrap with Default::default().

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • pnpm/pnpm#12296: Implements the same "package-manager bootstrap via trusted user/CLI registries" concept in TypeScript (getPackageManagerBootstrapConfig, syncEnvLockfile, switchCliVersion), the direct TS counterpart to this Rust implementation.
  • pnpm/pnpm#12340: Updates the same config_deps.rs resolver construction path to use trusted registry configuration, modifying overlapping code around dependency resolver setup.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: implementing trusted registry resolution for package-manager bootstrap, which is the core security fix described in the PR objectives.
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.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/pacquet-pm-bootstrap-registries

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.

@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: 1

🤖 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 `@pacquet/crates/config/src/lib.rs`:
- Around line 1451-1460: The `PackageManagerBootstrap` struct currently derives
Default which leaves the registry field as an empty string, causing
package-manager resolution to fail. Remove the Default derivation from the
struct definition and implement a custom Default trait for
PackageManagerBootstrap that explicitly sets the registry field to a proper
default value such as npmjs, while keeping the other fields at their default
values.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 85a0d671-8d18-4fd7-870e-7af9493b9414

📥 Commits

Reviewing files that changed from the base of the PR and between 268c97d and 8f17eaf.

📒 Files selected for processing (5)
  • pacquet/crates/cli/src/config_deps.rs
  • pacquet/crates/config/src/lib.rs
  • pacquet/crates/config/src/npmrc_auth.rs
  • pacquet/crates/config/src/tests.rs
  • pacquet/crates/package-manager/src/install_package_from_registry/tests.rs

Comment thread pacquet/crates/config/src/lib.rs Outdated
zkochan added a commit that referenced this pull request Jun 17, 2026
Give `PackageManagerBootstrap` a SmartDefault registry of the public npm
registry so a directly-constructed `Config` (one not finalized through
`Config::current`) never resolves the package manager against an empty
registry. Addresses a CodeRabbit review note on #12471.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

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

@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.00      8.2±0.19ms   526.2 KB/sec    1.01      8.3±0.13ms   520.5 KB/sec

@codecov-commenter

codecov-commenter commented Jun 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.82353% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.13%. Comparing base (de74c58) to head (5ad2b2d).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
pacquet/crates/cli/src/config_deps.rs 65.62% 11 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12471      +/-   ##
==========================================
- Coverage   88.13%   88.13%   -0.01%     
==========================================
  Files         311      311              
  Lines       41941    42001      +60     
==========================================
+ Hits        36966    37016      +50     
- Misses       4975     4985      +10     

☔ 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.

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

Copy link
Copy Markdown

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

@github-actions

github-actions Bot commented Jun 17, 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 4.110 ± 0.166 3.882 4.309 1.97 ± 0.14
pacquet@main 4.086 ± 0.141 3.885 4.329 1.96 ± 0.13
pnpr@HEAD 2.092 ± 0.113 1.961 2.355 1.00 ± 0.08
pnpr@main 2.086 ± 0.122 1.954 2.297 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.11024558938,
      "stddev": 0.16588416941104264,
      "median": 4.159666681179999,
      "user": 3.9602367999999992,
      "system": 3.3454005599999994,
      "min": 3.8818989046800003,
      "max": 4.309246872679999,
      "times": [
        4.309246872679999,
        4.196670279679999,
        3.8818989046800003,
        4.12266308268,
        4.22399863968,
        3.95626458168,
        4.22847305168,
        3.93448570468,
        4.306548030679999,
        3.94220674568
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.08581337958,
      "stddev": 0.1406831661500432,
      "median": 4.065699428179999,
      "user": 3.9227554999999996,
      "system": 3.39089666,
      "min": 3.88537039868,
      "max": 4.32894250668,
      "times": [
        4.155887593679999,
        3.98755640368,
        4.29217659068,
        4.01699719268,
        4.082269483679999,
        4.32894250668,
        4.098239286679999,
        3.88537039868,
        3.96156496668,
        4.0491293726799995
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.0923994773800003,
      "stddev": 0.11287185269685002,
      "median": 2.04350159868,
      "user": 2.7424879000000004,
      "system": 2.8773741599999996,
      "min": 1.9605278136800002,
      "max": 2.35518538668,
      "times": [
        2.19929009268,
        2.04030398668,
        2.04060359768,
        2.04223792768,
        1.9605278136800002,
        2.04476526968,
        2.35518538668,
        2.10225745568,
        2.12162420568,
        2.01719903768
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.08644769308,
      "stddev": 0.12190851346507806,
      "median": 2.0362280041800003,
      "user": 2.7297076999999996,
      "system": 2.8970473599999997,
      "min": 1.9539352566800001,
      "max": 2.29735101668,
      "times": [
        2.29735101668,
        2.0346393376800003,
        2.24061757268,
        2.23250039868,
        1.9539352566800001,
        1.9783661566800002,
        2.01823385268,
        2.0401019966800003,
        2.03091467168,
        2.0378166706800003
      ]
    }
  ]
}

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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 632.0 ± 20.8 598.1 666.8 1.00
pacquet@main 663.8 ± 69.4 609.4 849.3 1.05 ± 0.12
pnpr@HEAD 715.1 ± 93.8 662.6 976.1 1.13 ± 0.15
pnpr@main 712.8 ± 37.4 674.1 784.8 1.13 ± 0.07
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.63195399356,
      "stddev": 0.02081323031581742,
      "median": 0.62718181356,
      "user": 0.36630468,
      "system": 1.32967546,
      "min": 0.5981292665600001,
      "max": 0.6667986225600001,
      "times": [
        0.5981292665600001,
        0.6430002305600001,
        0.6488608185600001,
        0.6273146885600001,
        0.6667986225600001,
        0.6270489385600001,
        0.6188984515600001,
        0.6183188865600001,
        0.6168196235600001,
        0.65435040856
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.6638257988600001,
      "stddev": 0.06943327226263984,
      "median": 0.6395815880600001,
      "user": 0.39506628000000005,
      "system": 1.3219155599999999,
      "min": 0.6093876745600001,
      "max": 0.84934560156,
      "times": [
        0.6406800125600001,
        0.6195268305600001,
        0.6093876745600001,
        0.63839442556,
        0.6464929425600001,
        0.6641231205600001,
        0.6968261465600001,
        0.6384831635600001,
        0.6349980705600001,
        0.84934560156
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.7150839210600001,
      "stddev": 0.09380983963750163,
      "median": 0.6878681090600001,
      "user": 0.37953528,
      "system": 1.36389966,
      "min": 0.6626484235600001,
      "max": 0.9761252735600001,
      "times": [
        0.70826257256,
        0.6892563285600001,
        0.6864798895600001,
        0.66348050156,
        0.67776428056,
        0.6642165505600001,
        0.6626484235600001,
        0.7039894925600001,
        0.9761252735600001,
        0.7186158975600001
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.71275431016,
      "stddev": 0.037440461011519766,
      "median": 0.6991773515600002,
      "user": 0.4059740799999999,
      "system": 1.3655437600000002,
      "min": 0.67411085956,
      "max": 0.7847796705600001,
      "times": [
        0.7847796705600001,
        0.7000762975600001,
        0.7035489275600001,
        0.67411085956,
        0.6982784055600001,
        0.6971797575600001,
        0.6836081625600001,
        0.7711804755600001,
        0.7288014165600001,
        0.6859791285600001
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.255 ± 0.057 4.186 4.385 2.02 ± 0.12
pacquet@main 4.184 ± 0.038 4.118 4.238 1.98 ± 0.11
pnpr@HEAD 2.109 ± 0.120 1.939 2.308 1.00
pnpr@main 2.185 ± 0.144 2.008 2.440 1.04 ± 0.09
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.25492015668,
      "stddev": 0.05748769694724176,
      "median": 4.247289389880001,
      "user": 3.8023267599999997,
      "system": 3.28143106,
      "min": 4.18568931638,
      "max": 4.385194712380001,
      "times": [
        4.385194712380001,
        4.28088266338,
        4.20089699638,
        4.24390900838,
        4.2922881333800005,
        4.25066977138,
        4.23258900138,
        4.20841057438,
        4.18568931638,
        4.268671389380001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.18353184658,
      "stddev": 0.03769593480882368,
      "median": 4.18769521188,
      "user": 3.75624976,
      "system": 3.2697051599999996,
      "min": 4.11848411138,
      "max": 4.23797747538,
      "times": [
        4.23797747538,
        4.19527037838,
        4.14540143538,
        4.15862512538,
        4.18783148738,
        4.23092819038,
        4.163371451380001,
        4.18755893638,
        4.11848411138,
        4.20986987438
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.10865966908,
      "stddev": 0.12011924015100364,
      "median": 2.0733314483800003,
      "user": 2.5592941599999994,
      "system": 2.7725949599999997,
      "min": 1.93935436338,
      "max": 2.3075348193800003,
      "times": [
        2.06427796038,
        2.2863751433800004,
        2.0765519393800003,
        2.2053500723800004,
        2.02987027038,
        2.3075348193800003,
        2.0701109573800003,
        2.09802388738,
        1.93935436338,
        2.0091472773800003
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.18470637238,
      "stddev": 0.1442624470496162,
      "median": 2.1464054428800003,
      "user": 2.52960066,
      "system": 2.7689479599999998,
      "min": 2.0078610613800003,
      "max": 2.4396897113800002,
      "times": [
        2.37880285038,
        2.4396897113800002,
        2.1437702133800003,
        2.0463989713800004,
        2.2151442073800003,
        2.0078610613800003,
        2.2799534873800003,
        2.04566663638,
        2.1407359123800003,
        2.1490406723800004
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.353 ± 0.018 1.333 1.385 2.07 ± 0.13
pacquet@main 1.439 ± 0.080 1.393 1.666 2.20 ± 0.18
pnpr@HEAD 0.681 ± 0.045 0.659 0.808 1.04 ± 0.09
pnpr@main 0.655 ± 0.040 0.627 0.766 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.3533911659800002,
      "stddev": 0.01787899793670438,
      "median": 1.34527902668,
      "user": 1.3340099599999997,
      "system": 1.7041494199999998,
      "min": 1.3331922401800003,
      "max": 1.3850607001800002,
      "times": [
        1.3742941381800002,
        1.3428420951800002,
        1.3331922401800003,
        1.3406692581800002,
        1.3466189211800002,
        1.3386526971800001,
        1.34393913218,
        1.3552507551800002,
        1.3850607001800002,
        1.37339172218
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.4394403899800003,
      "stddev": 0.08044491839977713,
      "median": 1.41466105268,
      "user": 1.3716620599999998,
      "system": 1.80066242,
      "min": 1.39268398918,
      "max": 1.6659021861800003,
      "times": [
        1.39268398918,
        1.4153442921800001,
        1.6659021861800003,
        1.41110325318,
        1.43340369118,
        1.42718243818,
        1.40989824418,
        1.4018844301800002,
        1.4139778131800003,
        1.42302356218
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.68093222048,
      "stddev": 0.044963452224076945,
      "median": 0.66624974668,
      "user": 0.31431025999999995,
      "system": 1.3132282199999998,
      "min": 0.6592960171800001,
      "max": 0.80789164018,
      "times": [
        0.66552401218,
        0.66697548118,
        0.6762967521800001,
        0.66422343118,
        0.6592960171800001,
        0.80789164018,
        0.6635412901800001,
        0.66119422218,
        0.6682643111800001,
        0.67611504718
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.65486056948,
      "stddev": 0.039754929491520284,
      "median": 0.64717859968,
      "user": 0.33461746000000003,
      "system": 1.2735494200000002,
      "min": 0.6268782821800001,
      "max": 0.7656193611800001,
      "times": [
        0.63621788418,
        0.64691840518,
        0.64007308918,
        0.6347440201800001,
        0.6539285701800001,
        0.64743879418,
        0.64834367818,
        0.6268782821800001,
        0.7656193611800001,
        0.6484436101800001
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 3.010 ± 0.038 2.977 3.099 4.67 ± 0.09
pacquet@main 2.991 ± 0.027 2.967 3.057 4.64 ± 0.07
pnpr@HEAD 0.646 ± 0.010 0.633 0.659 1.00 ± 0.02
pnpr@main 0.644 ± 0.009 0.633 0.657 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 3.0101464081800007,
      "stddev": 0.03780752899032212,
      "median": 3.00107045238,
      "user": 1.7315262999999999,
      "system": 1.9796722999999996,
      "min": 2.9773782153800004,
      "max": 3.0992848413800003,
      "times": [
        3.0393461043800003,
        2.9773782153800004,
        3.0310262263800003,
        2.98678708038,
        2.9779892413800004,
        3.00366148238,
        2.9823220853800003,
        3.00518938238,
        3.0992848413800003,
        2.99847942238
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.9914275764800005,
      "stddev": 0.027286563645303578,
      "median": 2.98222837238,
      "user": 1.7485281000000001,
      "system": 1.9812271,
      "min": 2.9665855263800003,
      "max": 3.05707779538,
      "times": [
        3.00331898238,
        2.97668820138,
        2.9873804443800003,
        2.97714382738,
        2.98232791138,
        2.9681753953800003,
        2.9665855263800003,
        2.98212883338,
        3.05707779538,
        3.0134488473800003
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6458308821800001,
      "stddev": 0.00979708075321192,
      "median": 0.6468328698800001,
      "user": 0.3237181,
      "system": 1.2924101000000001,
      "min": 0.63262468538,
      "max": 0.6587839403800001,
      "times": [
        0.64181307438,
        0.6370795843800001,
        0.6572449613800001,
        0.64775513738,
        0.6326421123800001,
        0.63262468538,
        0.65669958638,
        0.6587839403800001,
        0.6474914583800001,
        0.6461742813800001
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.64418983328,
      "stddev": 0.00855594103605648,
      "median": 0.6410328508800001,
      "user": 0.34108150000000004,
      "system": 1.2663501000000001,
      "min": 0.63295111538,
      "max": 0.65680771138,
      "times": [
        0.63812392738,
        0.63295111538,
        0.64102767638,
        0.64021328838,
        0.6482129533800001,
        0.6550091933800001,
        0.6351050863800001,
        0.65680771138,
        0.6534093553800001,
        0.6410380253800001
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12471
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
4,254.92 ms
(+0.87%)Baseline: 4,218.39 ms
5,062.06 ms
(84.06%)
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
🚷 view threshold
3,010.15 ms
(-0.10%)Baseline: 3,013.03 ms
3,615.63 ms
(83.25%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,353.39 ms
(+1.67%)Baseline: 1,331.12 ms
1,597.35 ms
(84.73%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
4,110.25 ms
(-1.96%)Baseline: 4,192.59 ms
5,031.11 ms
(81.70%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
631.95 ms
(+2.44%)Baseline: 616.90 ms
740.29 ms
(85.37%)
🐰 View full continuous benchmarking report in Bencher

@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12471
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
2,108.66 ms
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
645.83 ms
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
680.93 ms
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,092.40 ms
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
715.08 ms
🐰 View full continuous benchmarking report in Bencher

zkochan added a commit that referenced this pull request Jun 17, 2026
Give `PackageManagerBootstrap` a SmartDefault registry of the public npm
registry so a directly-constructed `Config` (one not finalized through
`Config::current`) never resolves the package manager against an empty
registry. Addresses a CodeRabbit review note on #12471.
@zkochan zkochan force-pushed the fix/pacquet-pm-bootstrap-registries branch from d502bbb to 5ad2b2d Compare June 17, 2026 12:20
Comment on lines +139 to +154
/// Context for resolving the package manager pnpm auto-switches to
/// (`pnpm` / `@pnpm/exe`). Resolves through TRUSTED registries and
/// network config only — never the repository-controlled project
/// `.npmrc` / `pnpm-workspace.yaml` — so a malicious workspace cannot
/// redirect the package-manager bytes. See GHSA-j2hc-m6cf-6jm8.
fn for_package_manager(config: &Config) -> Result<Self> {
let bootstrap = &config.package_manager_bootstrap;
Self::build(
config,
&bootstrap.proxy,
&bootstrap.tls,
&bootstrap.tls_by_uri,
bootstrap.resolved_registries(),
Arc::clone(&bootstrap.auth_headers),
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Bootstrap reads repo cache 🐞 Bug ⛨ Security

EnvInstallerContext::for_package_manager switches registries/auth/proxy/TLS to trusted sources but
still builds the NpmResolver using Config.offline/prefer_offline and Config.cache_dir, which
pnpm-workspace.yaml can set. A malicious repo can set preferOffline/offline and point cacheDir at a
repo path containing a crafted metadata mirror so package-manager bootstrap resolves pnpm/@pnpm/exe
from attacker-controlled metadata and writes poisoned resolution/integrity into pnpm-lock.yaml.
Agent Prompt
## Issue description
`EnvInstallerContext::for_package_manager` uses trusted bootstrap registries/proxy/TLS/auth, but the resolver it builds still inherits `offline`, `prefer_offline`, and `cache_dir` from `Config`, which are repository-controlled via `pnpm-workspace.yaml`. Because the npm resolver reads the on-disk mirror first (and can return without any network fetch) when `offline`/`prefer_offline` is enabled, a malicious repo can supply crafted mirror metadata and poison `packageManagerDependencies` integrity/resolution written into `pnpm-lock.yaml`.

## Issue Context
- `pnpm-workspace.yaml` can set `offline`, `prefer_offline`, and `cache_dir`, and these are applied onto `Config`.
- The npm resolver consults the on-disk mirror when `offline` or `prefer_offline` is set, and in `offline` mode it will return the mirror result without a network request.
- The package-manager bootstrap path (`sync_package_manager_dependencies`) uses this resolver to populate `packageManagerDependencies` in the env lockfile.

## Fix Focus Areas
- pacquet/crates/cli/src/config_deps.rs[139-214]
- pacquet/crates/config/src/workspace_yaml.rs[121-129]
- pacquet/crates/config/src/workspace_yaml.rs[726-742]
- pacquet/crates/config/src/workspace_yaml.rs[917-919]
- pacquet/crates/resolving-npm-resolver/src/pick_package.rs[496-533]

## What to change
1. Ensure the package-manager bootstrap resolver does **not** accept repository-controlled cache/offline settings.
   - Option A (minimal, localized): In `EnvInstallerContext::for_package_manager`, override resolver config to a safe trusted posture (e.g. `offline=false`, `prefer_offline=false`, and use a non-workspace cache dir).
   - Option B (more complete/parity-friendly): Extend `PackageManagerBootstrap` to carry trusted-only `offline`, `prefer_offline`, and `cache_dir` (built from trusted sources only) and have `for_package_manager` use those fields.
2. Add a regression test demonstrating the attack:
   - A temp project with `pnpm-workspace.yaml` setting `preferOffline: true` and `cacheDir: ./.cache`.
   - Pre-populate the expected mirror path for `pnpm` under that cache dir with crafted metadata.
   - Assert bootstrap resolution incorrectly uses the mirror today; after the fix, it should fetch from the trusted registry (or at least ignore the repo cache).

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch, and it's a real concern — but it's deliberately out of scope for this PR, for parity with pnpm.

pnpm's current main has the same exposure: switchCliVersion() builds its store controller as createStoreController({ ...config, ...context, ...packageManagerConfig }), and packageManagerConfig (getPackageManagerBootstrapConfig) overrides only the registries + network config — it does not override cacheDir/storeDir/offline/preferOffline. So pnpm's package-manager bootstrap reads those same (workspace-influenceable) values. This PR mirrors that exactly: it closes the registry/network trust boundary of GHSA-j2hc-m6cf-6jm8 (the #12296 scope) and nothing more.

The cache/store trust boundary is a separate finding in the same campaign — upstream addresses it with dedicated trustedCacheDir/trustedStoreDir config that has not landed in main yet. Per the pacquet cardinal rule (match pnpm; don't add behavior that isn't upstream yet), hardening cache_dir/offline/prefer_offline for bootstrap should land in pnpm first and then be mirrored here, so the two stacks stay in lockstep. Doing it unilaterally in pacquet now would make pacquet diverge from pnpm.

Tracking it as follow-up rather than expanding this PR's scope.


Written by an agent (Claude Code, claude-opus-4-8).

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 5ad2b2d

@zkochan

zkochan commented Jun 17, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
pacquet/crates/config/src/lib.rs (1)

1863-1892: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t drop trusted registry settings outside .npmrc.

package_manager_bootstrap is rebuilt from Config::default() plus folded .npmrc auth sources, before the trusted global config.yaml layer is applied. That lets a user-level config.yaml registry — or any caller/CLI-populated self.registry — drive normal installs while package-manager auto-switch still falls back to npmjs. In private-mirror setups, pnpm / @pnpm/exe bootstrap bypasses the user’s trusted registry; thread trusted global/pre-populated registry values into the bootstrap before resolving package-manager dependencies. Upstream describes this path as using trusted config sources rather than project/workspace settings. (github.com)

As per coding guidelines, “Keep pnpm (TypeScript) and pacquet (Rust) behavior in sync” and review registry/network/auth trust-boundary issues first.

Also applies to: 2094-2119

🤖 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 `@pacquet/crates/config/src/lib.rs` around lines 1863 - 1892, The issue is that
when building package_manager_bootstrap, only the trusted auth information from
trusted_auth is being passed to build_package_manager_bootstrap::<Sys>(), but
trusted registry settings and network configuration from global config.yaml or
pre-populated self.registry values are being dropped. You need to extract and
preserve the registry and network settings from the trusted sources
(env_scoped_source, auth_ini_source, and user_source) and thread them into the
bootstrap construction along with trusted_auth to ensure the package-manager
auto-switch respects the user's trusted registry configuration rather than
falling back to the default npmjs registry.

Source: Coding guidelines

pacquet/crates/cli/src/config_deps.rs (1)

186-201: ⚠️ Potential issue | 🔴 Critical

The resolver for package-manager bootstrap still uses repository-controlled cache/offline settings, bypassing the GHSA-j2hc-m6cf-6jm8 fix.

for_package_manager() correctly routes through trusted bootstrap registries and auth to prevent a malicious workspace from redirecting package-manager bytes. However, the NpmResolver it builds still receives config.cache_dir, config.offline, and config.prefer_offline — all three of which are populated from pnpm-workspace.yaml via WorkspaceSettings::apply_to().

A malicious workspace can set offline: true and cache_dir: ./attacker-cache in its pnpm-workspace.yaml, forcing the resolver to serve poisoned packument metadata from a workspace-controlled directory and skip network validation. This bypasses the trust boundary intended by the GHSA-j2hc-m6cf-6jm8 fix.

Remove the three repository-controlled fields from the package-manager bootstrap resolver, or surface trusted versions from PackageManagerBootstrap.

🤖 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 `@pacquet/crates/cli/src/config_deps.rs` around lines 186 - 201, The
NpmResolver being initialized for package-manager bootstrap is receiving three
repository-controlled settings from config (cache_dir, offline, and
prefer_offline) that originate from pnpm-workspace.yaml, allowing a malicious
workspace to inject poisoned packument metadata and bypass the trust boundary.
Remove these three fields from the NpmResolver struct initialization where
cache_dir is set to Some(config.cache_dir.clone()), offline is set to
config.offline, and prefer_offline is set to config.prefer_offline, either by
using safe default values or by replacing them with trusted values from
PackageManagerBootstrap instead of the user-controlled config object.

Source: Coding guidelines

🧹 Nitpick comments (1)
pacquet/crates/config/src/tests.rs (1)

1665-1766: ⚡ Quick win

Add auth/proxy/TLS isolation assertions for the bootstrap surface.

These tests lock registry selection, but PackageManagerBootstrap also carries auth_headers, proxy, TLS, and per-registry TLS. Add a small fixture where project .npmrc sets attacker credentials/proxy/TLS and user .npmrc sets trusted ones, then assert package_manager_bootstrap contains only the trusted values. That keeps the security regression suite aligned with the full changed surface.

As per coding guidelines, use real fixtures such as tempfile::TempDir for tests and review registry/network/auth trust-boundary issues first.

🤖 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 `@pacquet/crates/config/src/tests.rs` around lines 1665 - 1766, Add a new test
function following the existing pattern of the registry isolation tests (like
package_manager_bootstrap_ignores_project_npmrc_registry) to verify that
auth_headers, proxy, and TLS settings from a project .npmrc are not included in
the package_manager_bootstrap configuration. Create a fixture with a user-level
.npmrc containing trusted auth and proxy settings, and a project .npmrc
containing attacker-controlled credentials and proxy settings. Use tempdir() to
create real temporary files for both configs, then call
load_with_project_and_user or Config::current to load the configuration and
assert that package_manager_bootstrap contains only the trusted user-level
values (auth_headers, proxy, and TLS settings) while the main config contains
the project-controlled values. This extends the security regression test suite
to cover the full trust boundary surface beyond just registry selection.

Source: Coding guidelines

🤖 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.

Outside diff comments:
In `@pacquet/crates/cli/src/config_deps.rs`:
- Around line 186-201: The NpmResolver being initialized for package-manager
bootstrap is receiving three repository-controlled settings from config
(cache_dir, offline, and prefer_offline) that originate from
pnpm-workspace.yaml, allowing a malicious workspace to inject poisoned packument
metadata and bypass the trust boundary. Remove these three fields from the
NpmResolver struct initialization where cache_dir is set to
Some(config.cache_dir.clone()), offline is set to config.offline, and
prefer_offline is set to config.prefer_offline, either by using safe default
values or by replacing them with trusted values from PackageManagerBootstrap
instead of the user-controlled config object.

In `@pacquet/crates/config/src/lib.rs`:
- Around line 1863-1892: The issue is that when building
package_manager_bootstrap, only the trusted auth information from trusted_auth
is being passed to build_package_manager_bootstrap::<Sys>(), but trusted
registry settings and network configuration from global config.yaml or
pre-populated self.registry values are being dropped. You need to extract and
preserve the registry and network settings from the trusted sources
(env_scoped_source, auth_ini_source, and user_source) and thread them into the
bootstrap construction along with trusted_auth to ensure the package-manager
auto-switch respects the user's trusted registry configuration rather than
falling back to the default npmjs registry.

---

Nitpick comments:
In `@pacquet/crates/config/src/tests.rs`:
- Around line 1665-1766: Add a new test function following the existing pattern
of the registry isolation tests (like
package_manager_bootstrap_ignores_project_npmrc_registry) to verify that
auth_headers, proxy, and TLS settings from a project .npmrc are not included in
the package_manager_bootstrap configuration. Create a fixture with a user-level
.npmrc containing trusted auth and proxy settings, and a project .npmrc
containing attacker-controlled credentials and proxy settings. Use tempdir() to
create real temporary files for both configs, then call
load_with_project_and_user or Config::current to load the configuration and
assert that package_manager_bootstrap contains only the trusted user-level
values (auth_headers, proxy, and TLS settings) while the main config contains
the project-controlled values. This extends the security regression test suite
to cover the full trust boundary surface beyond just registry selection.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 888c0cf0-97bc-400b-beb6-b839c9d32bcb

📥 Commits

Reviewing files that changed from the base of the PR and between 7a31307 and 5ad2b2d.

📒 Files selected for processing (5)
  • pacquet/crates/cli/src/config_deps.rs
  • pacquet/crates/config/src/lib.rs
  • pacquet/crates/config/src/npmrc_auth.rs
  • pacquet/crates/config/src/tests.rs
  • pacquet/crates/package-manager/src/install_package_from_registry/tests.rs

@github-actions github-actions Bot added the reviewed: coderabbit CodeRabbit submitted an approving review label Jun 17, 2026
…tries

Port the GHSA-j2hc-m6cf-6jm8 fix (#12296) to pacquet.

When pnpm auto-switches to the version requested by `packageManager` /
`devEngines.packageManager`, the bootstrap (`pnpm` / `@pnpm/exe`) must be
resolved through trusted registries only. Pacquet was resolving it through
`config.resolved_registries()`, which a malicious repository controls via
the workspace `.npmrc` or `pnpm-workspace.yaml` `registries:` block.

Add `Config::package_manager_bootstrap`, built in `Config::current()` from a
trusted-only fold of the URL-scoped env, `auth.ini`, and user `.npmrc`
sources (the project `.npmrc` is excluded), reusing the existing
registry/proxy/TLS/auth application logic. It defaults to the public npm
registry, and `PNPM_CONFIG_REGISTRY` still overrides the default because it
is user-controlled.

`EnvInstallerContext::for_package_manager` routes only the package-manager
bootstrap path (`sync_package_manager_dependencies`) through this trusted
config; project `configDependencies` resolution keeps the project
registries, matching the narrow scope of the upstream TypeScript fix.
@zkochan zkochan force-pushed the fix/pacquet-pm-bootstrap-registries branch from 5ad2b2d to 14dde1d Compare June 17, 2026 21:34
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 14dde1d

@zkochan zkochan merged commit 6e218db into main Jun 17, 2026
30 of 31 checks passed
@zkochan zkochan deleted the fix/pacquet-pm-bootstrap-registries branch June 17, 2026 22:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

product: pacquet reviewed: coderabbit CodeRabbit submitted an approving review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants