Skip to content

feat(pnpm): pnpm 7-12 vendor+hosted support — legacy grammars, zero-touch trustLockfile, takeover reconciliation, revert guards - #213

Merged
Mikola Lysenko (mikolalysenko) merged 4 commits into
fix/pnpm-lock-inventory-legacyfrom
feat/pnpm-legacy-and-zero-touch-trust
Aug 19, 2026
Merged

feat(pnpm): pnpm 7-12 vendor+hosted support — legacy grammars, zero-touch trustLockfile, takeover reconciliation, revert guards#213
Mikola Lysenko (mikolalysenko) merged 4 commits into
fix/pnpm-lock-inventory-legacyfrom
feat/pnpm-legacy-and-zero-touch-trust

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Closes the pnpm support gap end to end: every pnpm major since 7 now works in vendor and hosted mode, pnpm 11/12 hosted installs are zero-touch (no flags, no CI changes), and the conversion/revert/repair lifecycle around them is fail-closed. Everything here was built spike-first and verified against real corepack-pinned pnpm 7.33.5 / 8.15.9 / 9.15.9 / 10.34.5 / 11.22.0 / 12.0.0-rc.7 in the 2026-08-18 e2e campaign (54 matrix/probe legs), then adversarially reviewed (10 verified findings, all remediated in this diff, each RED-verified).

Stacked on #203 (lock-inventory legacy grammars — this diff extends the same file). Three test hunks overlap #204/#208 with identical content (env-var mirrors for the new global flag); merge those first and this rebases clean.

Hosted mode

  • pnpm 7/8 locks are rewritten, not refused: every instance key of a dep is spliced (v5 /name/ver + _peer-suffixed, v6 parenthesized-peer; each key owns its own resolution:), one revert-ledger edit per instance. A post-splice residual detector refuses the dep set-wide if any instance shape the splice cannot claim remains — no partial rewrites (review finding: nested-paren suffixes provably unmatchable by the regex are now a refusal, not a silent skip). Proven: frozen installs from empty stores land patched bytes on real pnpm 7/8; tamper fails ERR_PNPM_TARBALL_INTEGRITY.
  • Zero-touch pnpm 11/12: rewriting a v9 root lock auto-writes trustLockfile: true into pnpm-workspace.yaml (create-with-scaffold or byte-preserving merge; ledger edit redirect_pnpm_workspace_trust; --no-trust-lockfile-config opt-out). Hardened per review: only ErrorKind::NotFound creates (an unreadable existing file gets a warning, never an overwrite), and re-scans heal a missing config on already-redirected locks. Verified: pnpm 11.22 + 12-rc frozen installs succeed with no flags; 9/10 ignore the key; the sha512 pin still fails closed under trust. The warning names the actual spliced host (userinfo stripped, only spliced-lock hosts), both per-major error codes, and pre-empts pnpm 12's own pnpm clean --lockfile advice that silently unpatches; legacy locks get legacy-correct guidance (pnpm 7/8 reject --trust-lockfile).

Vendor mode

  • New pnpm-legacy backend for 5.4/6.0 locks (flavor-stamped so older binaries fail closed on revert): package.json pnpm.overrides + lock surgery emitting byte-exactly what those majors serialize (idempotent under pnpm's own re-lock; byte-identical revert). Discovered + documented inherent limit: pnpm ≤ 8 absolutizes file: specifiers, so frozen installs are path-bound — surfaced as vendor_pnpm_legacy_absolute_specifier; a moved checkout runs one pnpm install --offline (marker bytes verified). Windows-shaped canonical paths normalized (verbatim prefix stripped, forward slashes; synthetic-input unit tests — a Windows CI leg should confirm pnpm's own emission spelling).
  • Version allowlist keeps future lock versions refusing with version-aware remedies; vendor hints now name pnpm-workspace.yaml among committables and tell users to reinstall (the live tree keeps pre-vendor bytes by design).

Conversions, revert safety, repair

  • Takeover reconciliation for npm-family (was cargo-only): vendoring over a hosted-redirected purl drops that purl's redirect-ledger halves — artifact-uuid-anchored matching (version-exact; v5 underscore instance keys claimed; degraded record-less ledgers keep edits fail-closed rather than risk destroying a sibling version's revert originals) — and vendor_supersedes_redirect fires exactly once. vendor --revert byte-restores the hosted lock from the wiring originals.
  • Revert-brick fix (was silent data loss): all six npm-family backends now refuse to delete an artifact the live lock still references when a repair-reconstructed entry has no wiring (vendor_wiring_unknown_revert_blocked, recovery guidance included); repair stamps detected flavors so the right guard fires, and preserves corrupt artifacts when no rebuild source exists (forensics over ENOENT).
  • Legacy-era diagnostics: shrinkwrap.yaml projects (pnpm ≤ 2) get pnpm-flavored no-lockfile guidance and join scan's lockfile-only supplement; a vendored lock entry gets redirect_pnpm_entry_vendored instead of a misleading entry-not-found; --mode redirect parses as the legacy alias of hosted.

Tests

  • New e2e_redirect_pnpm_build.rs — the hosted capstone pnpm never had (npm/yarn/bun did): real corepack pnpm 7/8/9/10/11 legs (the pnpm 11 leg is the zero-touch proof + opt-out control), tamper negative, hermetic v5/v6 splice legs. 14/14 green locally including all gated legs.
  • e2e_vendor_pnpm_build.rs ladder: Anson (@10) primary, @9/Doug Rudolph (@11) opportunistic, real pnpm 7/8 full-lifecycle legs (same-path frozen+offline proof, moved-checkout recovery, byte-identical revert).
  • Reconciliation, residual-detector, trust-hardening, revert-guard, and flavor-stamping suites — RED-verified where behavior changed. Golden redirect fixtures byte-identical (shared with the TS backend, untouched).
  • CLI_CONTRACT.md (new flag, warning codes, hosted pnpm contract, marker records) and docs/ecosystems.md updated.

Review status

Two adversarial review rounds ran over this campaign's diffs (10 + 10 verified findings); all are fixed here or in the sibling PRs #202/#203/#204/#208. Known accepted residuals are documented in code/PR-body where they live (rollback fan-out of patch-added files in store twins fails toward patched bytes; pnpm ≤ 3 custom non-registry.* hosts; Windows emission spelling needs a Windows CI leg).

🤖 Generated with Claude Code


Note

Medium Risk
Changes lockfile and workspace mutation paths, redirect/vendor ledger persistence, and install-time pnpm trust behavior—high impact for CI installs but heavily covered by new e2e and contract tests.

Overview
pnpm hosted (scan --mode hosted) now rewrites lockfileVersion 5.4/6.0/9.0 grammars (not only v9), with pnpm-specific no-lockfile diagnostics and clearer warnings when a lock entry is already vendored. For root v9 locks, the CLI can auto-write trustLockfile: true into pnpm-workspace.yaml (ledger edit redirect_pnpm_workspace_trust), with heal-on-rerun when the lock was already redirected, safe handling of unreadable workspace files, and opt-out via new global --no-trust-lockfile-config / SOCKET_NO_TRUST_LOCKFILE_CONFIG. Legacy locks get guidance that does not recommend --trust-lockfile on pnpm 7/8.

Vendor / repair / mode switches: Documents and implements pnpm-legacy vendor flavor for 5.4/6.0 locks; post-vendor human hints name pnpm-workspace.yaml for pnpm ≥11 and suggest reinstall commands so node_modules catches rewired locks. When vendoring supersedes hosted redirects, npm-family purls can automatically drop matching halves from redirect-state.json (with reconciled vs manual warning text). repair stamps reconstructed ledger entries with the npm lock flavor from the referencing lockfile, moves vendor_wiring_unknown to run-level warnings[], and defers deleting corrupt vendor artifacts until a rebuild source exists.

Contracts & tests: CLI_CONTRACT.md and rollback JSON semantics (package_not_installed marker records) are updated; new e2e_redirect_pnpm_build.rs and expanded CLI/apply invariant tests pin trust config, splice grammars, and unmatched-purl exit behavior.

Reviewed by Cursor Bugbot for commit 2d49af0. Configure here.

…rs, zero-touch trustLockfile, takeover reconciliation, revert guards

Built and verified against real corepack-pinned pnpm 7.33.5 / 8.15.9 /
9.15.9 / 10.34.5 / 11.22.0 / 12.0.0-rc.7 (plus legacy 1-6 probes) in the
2026-08-18 e2e campaign.

HOSTED, pnpm 7/8: the v5.4/v6.0 refusal is replaced by a real rewrite —
every instance key of a dep is spliced (v5 /name/ver and _peer-suffixed,
v6 (peer)-parenthesized; each owns its resolution), one ledger edit per
instance; a post-splice residual detector refuses the dep set-wide if any
instance shape the splice regex cannot claim remains (no partial
rewrites). Frozen installs from empty stores land patched bytes on both
majors; tamper fails ERR_PNPM_TARBALL_INTEGRITY.

HOSTED, pnpm 11/12 zero-touch: rewriting a v9 root lock now auto-writes
trustLockfile: true into pnpm-workspace.yaml (create with scaffold or
byte-preserving append; ledger-recorded as redirect_pnpm_workspace_trust;
--no-trust-lockfile-config opt-out; only ErrorKind::NotFound creates —
an unreadable existing file falls back to guidance, never overwrite;
re-scans heal a missing config on already-redirected locks). pnpm 11.22
and 12-rc frozen installs succeed with no flags and no CI changes; 9/10
ignore the key (verified); the sha512 pin still fails closed under trust.
Warnings name the actual spliced host (userinfo stripped) and both
per-major error codes, and pre-empt pnpm 12's own rebuild-the-lock advice
that silently unpatches.

VENDOR, pnpm 7/8: new pnpm-legacy backend (flavor-stamped so older
binaries fail closed) — package.json pnpm.overrides + legacy lock surgery
emitting exactly what those majors serialize (byte-stable under pnpm's
own re-lock). pnpm <= 8 absolutizes file: specifiers, so frozen installs
are path-bound: surfaced as vendor_pnpm_legacy_absolute_specifier, with
plain `pnpm install --offline` as the moved-checkout path (marker bytes
verified). Windows-shaped canonical paths are normalized (verbatim prefix
stripped, forward slashes).

CONVERSIONS + SAFETY: vendoring over a hosted-redirected npm-family purl
now reconciles the redirect ledger (artifact-uuid-anchored matching —
version-exact, v5 underscore keys claimed; degraded ledgers keep edits
fail-closed), firing vendor_supersedes_redirect exactly once; vendor
--revert byte-restores the hosted lock from the wiring originals. All six
npm-family vendor backends refuse to delete an artifact the live lock
still references when a repair-reconstructed entry has no wiring
(vendor_wiring_unknown_revert_blocked — the revert-brick fix); repair
stamps detected flavors and preserves corrupt artifacts when no rebuild
source exists. Legacy-era diagnostics: shrinkwrap.yaml projects get
pnpm-flavored no-lockfile guidance and join the lockfile-only supplement;
vendored lock entries get redirect_pnpm_entry_vendored instead of
entry-not-found.

Tests: e2e_redirect_pnpm_build.rs (new hosted capstone: pnpm 7-11 real
corepack legs incl. the zero-touch pnpm 11 proof, tamper negative,
hermetic v5/v6 legs), e2e_vendor_pnpm_build.rs ladder (@9/@10/@11 + real
pnpm 7/8 lifecycle legs), takeover/reconciliation/guard/heal unit + e2e
suites — all RED-verified where behavior changed. CLI_CONTRACT.md and
docs/ecosystems.md updated.

Stacked on #203 (lock-inventory legacy grammars); trivially overlapping
test hunks with #204/#208 carry identical content.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…(pnpm defaults frozen on under CI)

CI caught what local runs could not: pnpm turns --frozen-lockfile ON when
CI=true, and the pnpm <= 8 moved-checkout recovery works precisely by
re-resolving the path-bound absolute specifier — frozen semantics skip
that re-resolution (pnpm 8: ERR_PNPM_OUTDATED_LOCKFILE; pnpm 7:
stale-path install). The lifecycle legs' recovery step now passes
--no-frozen-lockfile explicitly, and the vendor_pnpm_legacy_absolute_
specifier remedy (warning text, module doc, CLI_CONTRACT.md,
docs/ecosystems.md) recommends `pnpm install --offline
--no-frozen-lockfile` so real CI users get working advice.

Verified: the full capstone (10 legs incl. real pnpm 7/8 lifecycles)
passes under CI=true locally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… byte-exactness)

Windows CI proved the production normalizer right and the test oracle
wrong: the hermetic splice legs built their expected absolute specifier
with raw canonicalize().display() — the \\?\C:\ verbatim form the
normalizer exists to strip. normalize_canonical_root is now pub and the
oracle consumes it at both assertion sites, so the expected string is
built by the same transformation the backend writes and cannot drift.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Windows CI surfaced the same oracle-drift bug in the module's own unit
tests: the fixture helper handed raw canonicalize().display() (verbatim
\\?\C:\ form) to the ROOT_TOKEN substitution, the no-leak contains probe,
and the moved-checkout fixture builder. All three now go through a
canon_root_str() helper built on normalize_canonical_root, so every
oracle spells the root exactly as the splice writes it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mikolalysenko
Mikola Lysenko (mikolalysenko) merged commit bdc47f4 into fix/pnpm-lock-inventory-legacy Aug 19, 2026
172 of 175 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the feat/pnpm-legacy-and-zero-touch-trust branch August 19, 2026 14:54
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.

1 participant