feat: Phase 1 of allowScripts opt-in install-script policy#9360
Open
JamieMagee wants to merge 7 commits into
Open
feat: Phase 1 of allowScripts opt-in install-script policy#9360JamieMagee wants to merge 7 commits into
allowScripts opt-in install-script policy#9360JamieMagee wants to merge 7 commits into
Conversation
…low-all-scripts configs
Three new configs to support the install-script opt-in policy. None
of them affect install behaviour yet; they're read by approve-scripts,
deny-scripts, and the install-time walker in later commits.
- allow-scripts: comma-separated package list. Used as a fallback
when the root package.json has no allowScripts field. Flattens
to flatOptions.allowScripts.
- strict-script-builds: boolean. Reserved for a future release that
will turn blocked-script warnings into errors. No-op for now.
- dangerously-allow-all-scripts: boolean escape hatch for that same
future release. No-op for now.
Refs: npm/rfcs#868
…I configs
A precedence resolver reads the install-time allowScripts policy from
the layered sources and threads it through install/ci into arborist.
- lib/utils/resolve-allow-scripts.js: pure resolver. Reads from
npm.prefix so workspace sub-installs still pick up the project
root. Returns { policy, source }. Strict fallback: package.json
wins over flat config; lower layers are silently ignored, with
one warn when a lower setting is being suppressed.
- install.js / ci.js: await the resolver before constructing
arborist opts, then pass policy through opts.allowScripts. Add
the three new params to each command's static params list.
- workspaces/arborist/lib/arborist/index.js: accept
options.allowScripts and store it on this.options. No enforcement
yet; read in later commits.
Also tightened the flatten function for the new allow-scripts config:
nopt wraps single comma-separated strings in arrays for [String, Array]
types, so each array entry needs splitting on commas before use.
Refs: npm/rfcs#868
A pure isScriptAllowed(node, policy) helper in
workspaces/arborist/lib/script-allowed.js. Used by the install-time
warning walker and by the approve-scripts / deny-scripts commands.
Matching rules follow the RFC:
- registry deps: name + optional semver (range or exact)
- git deps: canonical ssh-url match plus short-SHA prefix
- file / directory / remote tarball: exact resolved string match
- alias spec keys are ignored entirely; a user must address the real
package name, not the alias
- matching uses node.packageName, never node.name, so an alias
install cannot be approved by writing its alias name
Conflict resolution: any matching false wins over any matching true.
No match returns null (unreviewed).
Pure function, no I/O. 15 test cases cover alias safety and
omitLockfileRegistryResolved.
Refs: npm/rfcs#868
…tall scripts
The Phase 1 advisory warning. No scripts are blocked. After arb.reify()
completes, reify-finish walks the actual tree for dependencies whose
install-relevant lifecycle scripts are not yet covered by the
allowScripts policy. The result is appended to the install output as
one grouped block, not one log line per package.
- workspaces/arborist/lib/install-scripts.js: per-node helper that
returns the install-relevant lifecycle scripts. Covers preinstall,
install, postinstall, prepare (non-registry sources only), and the
synthetic 'node-gyp rebuild' detected by isNodeGypPackage from
@npmcli/node-gyp. The runtime fs check is needed because the
lockfile's hasInstallScript field misses packages whose only
install-time work is binding.gyp.
- lib/utils/check-allow-scripts.js: walks arb.actualTree.inventory
and filters to unreviewed nodes. Honours --ignore-scripts and
--dangerously-allow-all-scripts as full opt-outs. Treats explicit
deny entries as reviewed (no warning).
- lib/utils/reify-finish.js: runs the walker and passes results to
reify-output as an extras payload.
- lib/utils/reify-output.js: prints the grouped summary after the
funding and audit messages. JSON output puts the same data on
summary.unreviewedScripts.
Refs: npm/rfcs#868
Both share an implementation in lib/utils/allow-scripts-cmd.js; the
files in lib/commands/ are thin shims that set verb = 'approve' or
'deny'.
- npm approve-scripts <pkg>: writes 'pkg@version': true (pinned)
- npm approve-scripts --no-pin <pkg>: writes 'pkg': true (name-only)
- npm approve-scripts --all: approves every unreviewed install-script
package in the resolved actual tree
- npm approve-scripts --pending: read-only walk, lists unreviewed
packages without modifying package.json
- npm deny-scripts <pkg>: writes 'pkg': false. Always name-only,
regardless of --pin, per the RFC's asymmetric-pin rule.
- npm deny-scripts --all: denies every unreviewed install-script
package
The shared writer in lib/utils/allow-scripts-writer.js implements the
RFC's pin-mismatch table as pure functions:
applyApprovalForPackage(existing, nodes, { pin }) and
applyDenyForPackage(existing, nodes). Grouping by package matters
because a per-node API can't tell a stale pin from a newly installed
version; a per-package one can.
Two new flags registered in workspaces/config/lib/definitions
(--all reuses the existing global definition):
- --pending: read-only mode for approve-scripts
- --pin: control pin behaviour for approve-scripts (default true)
Includes docs/lib/content/commands/{npm-approve-scripts,npm-deny-scripts}.md.
Refs: npm/rfcs#868
The allowScripts policy must live at the project root. A non-root workspace declaring its own allowScripts field is almost always a mistake: that policy would be silently ignored at install time. reify-finish now walks the resolved actual tree after reify completes and emits one warning per non-root workspace whose package.json has an allowScripts field. Pure detection lives in lib/utils/warn-workspace-allow-scripts.js; the tree walk piggybacks on the inventory that's already loaded for the unreviewed-scripts summary. Refs: npm/rfcs#868
More tests for the Phase 1 install-script work, plus a handful of
/* istanbul ignore next */ markers for defensive fallbacks that can't
be hit from realistic inputs (e.g. nodes with neither packageName nor
name set, npa() failing on an already-validated string).
- reify-output: JSON path with unreviewedScripts and the node.name
fallback when packageName is absent
- approve-scripts: single-package wording, no-op when already at the
right pin, --all with no unreviewed packages
- deny-scripts: --all empty, global rejection, --json output, no-op
on already-denied
- allow-scripts-writer: null node guards, non-hosted git URL, absolute
path resolved field, deny entries on file/remote deps, alias-typed
policy keys, unparseable existing entries
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements Phase 1 of npm/rfcs#868, which makes dependency install scripts opt-in.
Install behaviour is unchanged. Scripts still run as they always have. The only Phase 1 user-visible change is one advisory block at the end of
npm installlisting packages whose install scripts haven't been reviewed via the newallowScriptsfield inpackage.json. A future release will turn that advisory into an actual block.What landed
allowScriptsfield inpackage.json, read at install timeallow-scripts,strict-script-builds,dangerously-allow-all-scripts. The latter two are no-ops in this release. They're registered so projects can pin them in tooling ahead of the release that flips the default.npm approve-scriptsandnpm deny-scriptscommands, with the RFC's asymmetric pin rule (approves can pin, denies are always name-only)npm install,ci,update, andrebuild.npm exec/npxconsult only the user/global.npmrclayer per the RFC, with the policy threaded through libnpmexec for Phase 2 enforcement.@npmcli/arboristcovering registry, git, file, and remote tarballs. Registry identity is derived from the lockfile's resolved URL (viaversionFromTgz), never fromnode.packageNameornode.version. Those getters read the installed tarball'spackage.jsonand can be forged.trusted@npm:naughty@1.0.0is approved by writingnaughty, nottrusted. Holds even underomitLockfileRegistryResolved, where the install location alone (node_modules/trusted) would be misleading. The underlying name is derived from the incoming edge's aliassubSpec.npm approve-scripts --alland positional matches. Per RFC they cannot be allowlisted in Phase 1.allowScriptsWhat's deliberately deferred
arb.rebuild()'s build set still runs everything. Phase 2 will gate#addToBuildSeton the matcher.parent@1.2.3 > bundled-nameform for a follow-up.Refs: npm/rfcs#868