Files
pnpm/deny.toml
Zoltan Kochan 54d2b57000 feat(pnpr): server-accelerated installs via pnprServer (endpoints + client + CLI) (#12077)
## What

Adds an opt-in **`pnprServer`** setting that offloads the slow part of an install — dependency resolution and computing which files the local store is missing — to a [pnpr](https://github.com/pnpm/pnpm/tree/main/pnpr) server, which streams back only the missing files. `node_modules` is still linked **locally** from the server-produced lockfile (like server-side rendering: the compute runs remotely, the result is materialized locally).

Realizes the agent concept from [RFC #9](https://github.com/pnpm/rfcs/pull/9), reworked around how it's actually used and rewritten in Rust on pacquet + pnpr.

## How it works

1. `pacquet install` (with `pnprServer` set) handshakes the server — `GET /-/pnpr` — to negotiate a protocol version.
2. It `POST`s `/v1/install` with the project's dependencies, the integrities already in its store, and **its own registry config** (default `registry`, `namedRegistries`, `overrides`, `minimumReleaseAge`).
3. The server resolves against *those* registries, fetches any uncached packages into its store, and streams NDJSON: `D` (missing file digests), `I` (pre-packed store-index entries), `L` (lockfile + stats).
4. The client downloads the missing files from `/v1/files` (gzip binary), writes them into its CAFS **by digest** (no re-hashing), writes the index entries, and runs a frozen install to link `node_modules` from the server's lockfile.

## Pieces

- **Server (`pnpr`)** — `GET /-/pnpr` handshake + `POST /v1/install` (NDJSON) + `POST /v1/files` (gzip), additive and opt-in alongside the npm-compatible API. Resolves against the client-sent registries, interning a `&'static Config` per distinct client config to bound the leak.
- **Client (`pacquet-pnpr-client`)** — `PnprClient`: reads store integrities, negotiates the protocol version, sends the registry config, parses the stream, materializes files + index entries, returns the lockfile. Rejects unrequested file entries and repairs truncated CAFS files.
- **CLI** — the `pnprServer` setting (`--pnpr-server`, `pnprServer:` in `pnpm-workspace.yaml`, `PNPM_CONFIG_PNPR_SERVER`). When set, `pacquet install` routes through the client and then links locally — pnpm's `install()` → `installFromPnpmRegistry` shape. `trustPolicy: no-downgrade` is refused (the server can't enforce it), matching pnpm's `TRUST_POLICY_INCOMPATIBLE_WITH_AGENT`.

## Design notes

- **A distinct URL, not the registry.** The server resolves from the registries the client sends, so it's a compute service — not "a registry that resolves from itself" — which is why it's a separate `pnprServer` URL rather than reusing `registry`. The same server works for any client's registry setup, and a single pnpr can be both registry and `pnprServer`.
- **Handshake = version negotiation + fail-fast.** Explicit opt-in, so there's no silent fallback to local resolution; a non-pnpr server (404) or a version mismatch errors clearly.
- **Naming:** everything is `pnpr`; "agent" survives only in upstream citations (`@pnpm/agent.client`, the pnpm-agent PoC, pnpm's `TRUST_POLICY_INCOMPATIBLE_WITH_AGENT` error code).

## Tests

- `pacquet-pnpr-client`: resolve + download, multi-file package, warm-store no-op, and handshake rejection. The pnpr server's own uplink is left at the default, so resolution provably uses the **client-sent** registry.
- `pacquet-cli`: a real `pacquet install --pnpr-server <url>` against an in-process pnpr (resolving from the mocked fixtures registry) links `node_modules`.
- `pnpr`: `/v1/files` binary-framing round-trip + handshake route.

Full suites green; clippy / dylint (Perfectionist) / fmt / taplo / `cargo doc -D warnings` clean.

## Deferred

Auth/credential forwarding (so private/scoped registries resolve), `pacquet add` / `remove` via `pnprServer`, multi-project workspaces, and true streaming (responses are buffered today).

Refs https://github.com/pnpm/rfcs/pull/9
2026-05-31 16:50:20 +02:00

125 lines
5.0 KiB
TOML

# Configuration for cargo-deny (https://embarkstudios.github.io/cargo-deny/).
# The schema evolves fast; fields follow the 0.19+ format.
# --- Graph ---------------------------------------------------------------
[graph]
targets = []
all-features = false
no-default-features = false
# --- Output --------------------------------------------------------------
[output]
feature-depth = 1
# --- Advisories ----------------------------------------------------------
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
[advisories]
db-path = "~/.cargo/advisory-db"
db-urls = ["https://github.com/rustsec/advisory-db"]
# Scope for RUSTSEC unmaintained advisories.
# One of "all", "workspace", "transitive", "none".
unmaintained = "workspace"
# yanked-crates check: "deny" | "warn" | "allow"
yanked = "warn"
ignore = [
# hickory-proto 0.25.2 is pulled in transitively through reqwest 0.13.x ->
# hickory-resolver 0.25.x. The reqwest 0.13 line has not migrated to
# hickory-proto 0.26, so `cargo update` cannot resolve either advisory; the
# only paths forward are an upstream reqwest release on hickory 0.26 or
# dropping the `hickory-dns` reqwest feature, which would regress the macOS
# `mDNSResponder` / `EAI_NONAME` workaround landed in #302. Revisit when
# reqwest moves to hickory 0.26.
#
# NSEC3 closest-encloser proof unbounded loop in `DnssecDnsHandle`. The
# vulnerable path is only linked when hickory-proto is built with the
# `dnssec-ring` or `dnssec-aws-lc-rs` Cargo feature; reqwest's `hickory-dns`
# feature does not enable either, so the affected code is unreachable in
# pacquet. The advisory itself notes "No safe upgrade is available" for the
# 0.25 line.
{ id = "RUSTSEC-2026-0118", reason = "DNSSEC validation path is not linked: reqwest's `hickory-dns` feature does not enable hickory-proto's `dnssec-ring`/`dnssec-aws-lc-rs` features, and no fix exists on the 0.25 line." },
# O(n²) name compression in `BinEncoder` during DNS message encoding.
# Reachability is bounded: the BinEncoder is only invoked when reqwest's
# `hickory-dns` resolver builds outbound DNS queries for the registry
# hostnames pacquet resolves, which originate from `.npmrc` and so can be
# attacker-influenced in an untrusted-checkout / untrusted-CI scenario. We
# accept this temporary DoS risk because no upgrade is reachable: reqwest
# 0.13.x (latest 0.13.3) is locked to `hickory-resolver` 0.25, and the fix
# ships only in `hickory-proto` 0.26.1+. Revisit when reqwest moves to
# hickory 0.26.
{ id = "RUSTSEC-2026-0119", reason = "Temporary risk acceptance: reqwest 0.13.x (latest 0.13.3) is locked to hickory-resolver 0.25 and no release consumes hickory-proto 0.26.1+ yet; revisit on reqwest upgrade." },
]
# --- Licenses ------------------------------------------------------------
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
[licenses]
allow = [
"MIT",
"MPL-2.0", # required by mockito, used by crates/tarball tests and tasks/micro-benchmark
"Apache-2.0",
"Unicode-3.0", # newer ICU crates switched from Unicode-DFS-2016 to this
"Unicode-DFS-2016",
"BSD-3-Clause",
"BSL-1.0",
"CDLA-Permissive-2.0", # `webpki-root-certs`, pulled in by reqwest's `rustls` feature
"ISC",
"Zlib", # required by foldhash, a transitive dep of rusqlite
]
confidence-threshold = 0.8
exceptions = [
# `pnpr` and `pnpr-fixtures` are first-party crates licensed under
# PolyForm Shield (see `pnpr/LICENSE.md`), not the MIT used by the rest
# of the workspace. cargo-deny only recognizes the PolyForm-Noncommercial
# identifier for this license text, so allow that id for just these two
# crates (the clarify entries below pin it by file hash).
{ name = "pnpr", allow = ["PolyForm-Noncommercial-1.0.0"] },
{ name = "pnpr-fixtures", allow = ["PolyForm-Noncommercial-1.0.0"] },
]
[[licenses.clarify]]
name = "pnpr"
expression = "PolyForm-Noncommercial-1.0.0"
license-files = [{ path = "../../LICENSE.md", hash = 0x652a978e }]
[[licenses.clarify]]
name = "pnpr-fixtures"
expression = "PolyForm-Noncommercial-1.0.0"
license-files = [{ path = "../../LICENSE.md", hash = 0x652a978e }]
[[licenses.clarify]]
name = "ring"
version = "*"
expression = "MIT AND ISC AND OpenSSL"
license-files = [
{ path = "LICENSE", hash = 0xbd0eed23 },
]
[licenses.private]
ignore = false
registries = []
# --- Bans ----------------------------------------------------------------
# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
[bans]
multiple-versions = "warn"
wildcards = "allow"
highlight = "all"
workspace-default-features = "allow"
external-default-features = "allow"
allow = []
deny = []
skip = []
skip-tree = []
# --- Sources -------------------------------------------------------------
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
[sources]
unknown-registry = "warn"
unknown-git = "warn"
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
allow-git = []
[sources.allow-org]
github = []
gitlab = []
bitbucket = []