Skip to content

RFC: pnpm agent — server-side resolution for faster installs#9

Open
zkochan wants to merge 1 commit into
pnpm:mainfrom
zkochan:pnpm-agent
Open

RFC: pnpm agent — server-side resolution for faster installs#9
zkochan wants to merge 1 commit into
pnpm:mainfrom
zkochan:pnpm-agent

Conversation

@zkochan

@zkochan zkochan commented Apr 14, 2026

Copy link
Copy Markdown
Member

This RFC proposes a pnpm agent — an opt-in server that resolves dependencies server-side and streams only the files missing from the client's store.

Reference implementation: pnpm/pnpm#11251

Key ideas

  • Server resolves dependencies using pnpm's own install() with SQLite-backed metadata cache (~1s resolution vs ~3.4s)
  • File-level dedup: only streams individual files the client is missing, not whole packages
  • Streaming NDJSON protocol: file downloads overlap with server-side resolution
  • Gzip-compressed file streaming: 274MB → ~80MB over the wire
  • Client skips: metadata download, tarball decompression, SHA-512 rehashing, store integrity verification

Configuration

# pnpm-workspace.yaml
agent: http://agent.company.com:4873

See the full RFC text for details.

zkochan added a commit to pnpm/pnpm that referenced this pull request Apr 14, 2026

@gluxon gluxon left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is a great idea. Definitely in support of the RFC.

zkochan added a commit to pnpm/pnpm that referenced this pull request Apr 16, 2026
zkochan added a commit to pnpm/pnpm that referenced this pull request Apr 16, 2026
zkochan added a commit to pnpm/pnpm that referenced this pull request Apr 17, 2026
@zkochan

zkochan commented May 29, 2026

Copy link
Copy Markdown
Member Author

This will be part of pnpr: https://github.com/pnpm/pnpm/tree/main/pnpr/npm/pnpr#pnpmpnpr

zkochan added a commit to pnpm/pnpm that referenced this pull request May 29, 2026
Port the pnpm-agent proof of concept from TypeScript onto pacquet's
resolver and content-addressable store, exposed as additive, opt-in,
versioned endpoints alongside pnpr's npm-compatible API:

- POST /v1/install resolves a project server-side (pacquet Install in
  lockfile-only mode against a throwaway temp project), fetches only the
  uncached packages into the shared store, and streams an NDJSON
  response: D lines (file digests the client is missing), I lines
  (pre-packed msgpackr-records store-index entries), and a final L line
  with the lockfile and stats.
- POST /v1/files serves a batch of files by digest as a gzip binary
  stream the client writes straight into its CAFS.

The pacquet runtime (a single leaked Config plus a shared HTTP client)
is held lazily per server in router state, so servers that never receive
an agent request pay nothing and each server in a multi-server test
process keeps its own store.

Adds a public Lockfile::load_wanted_from_dir mirroring pnpm's
readWantedLockfile(dir).

Deferred (documented in the module): multi-project workspaces,
incremental resolution from a client lockfile, overrides, per-request
minimumReleaseAge, and true response streaming.

Refs pnpm/rfcs#9

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

zkochan commented May 30, 2026

Copy link
Copy Markdown
Member Author

I am going to license the new server part with a source-available license instead of MIT: pnpm/pnpm#12082

So I am not sure if the rfc can be merged here or it should be a separate process for RFCs related to pnpr.

zkochan added a commit to pnpm/pnpm that referenced this pull request May 30, 2026
Port the pnpm-agent proof of concept from TypeScript onto pacquet's
resolver and content-addressable store, exposed as additive, opt-in,
versioned endpoints alongside pnpr's npm-compatible API:

- POST /v1/install resolves a project server-side (pacquet Install in
  lockfile-only mode against a throwaway temp project), fetches only the
  uncached packages into the shared store, and streams an NDJSON
  response: D lines (file digests the client is missing), I lines
  (pre-packed msgpackr-records store-index entries), and a final L line
  with the lockfile and stats.
- POST /v1/files serves a batch of files by digest as a gzip binary
  stream the client writes straight into its CAFS.

The pacquet runtime (a single leaked Config plus a shared HTTP client)
is held lazily per server in router state, so servers that never receive
an agent request pay nothing and each server in a multi-server test
process keeps its own store.

Adds a public Lockfile::load_wanted_from_dir mirroring pnpm's
readWantedLockfile(dir).

Deferred (documented in the module): multi-project workspaces,
incremental resolution from a client lockfile, overrides, per-request
minimumReleaseAge, and true response streaming.

Refs pnpm/rfcs#9

---
Written by an agent (Claude Code, claude-opus-4-8).
zkochan added a commit to pnpm/pnpm that referenced this pull request May 31, 2026
…lient + 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](pnpm/rfcs#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 pnpm/rfcs#9
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants