Skip to content

node packages: private libexec prefixes + guard against a second npm (#751) - #753

Merged
bryan-minimal merged 3 commits into
mainfrom
node-private-libexec
Sep 23, 2026
Merged

bryan-minimal merged 3 commits into
mainfrom
node-private-libexec

Conversation

@bryan-minimal

@bryan-minimal bryan-minimal commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

Companion to #752. Covers the two follow-ups from #751.

1. Out of the shared usr/lib/node_modules

cf, wrangler, next, mermaid-cli and capy ran npm install -g --prefix=$OUTPUT_DIR/usr, which writes into usr/lib/node_modules, the tree where the Node runtime's own npm lives. Tom warned about exactly this on #370. They now install into a private usr/libexec/<pkg> prefix, with relative usr/bin symlinks, the same pattern as vlt and pnpm.

  • Bins are linked by name, from the list the npm registry declares for each package: cf, wrangler/wrangler2/cf-wrangler, capy, next. Linking by name means a package can't drop an unclaimed or colliding file into usr/bin. A test -e on each link fails the build if upstream renames a bin, instead of shipping a dangling link.
  • mermaid-cli: the mmdc wrapper and install_check point at the new path, and the build checks that src/cli.js landed.
  • next: the sharp source build writes into the new NEXT_DIR, and the two require(...) tests use the new path.
  • Nothing else in the repo referenced the old paths, and nothing sets NODE_PATH. Node never resolves require from the global prefix anyway, so projects are unaffected.

2. node-runtime-guard (blocking)

This is the structural guard Karl suggested. A general "no two closure members ship different bytes at one path" check needs built trees, so it belongs in minimal itself. The npm case of it has static causes that are visible in the evaluated catalog, and this guard rejects them. It runs minimal dump -p --format json for amd64 and arm64 and checks every package except node/node-lts themselves:

Rule Rejects
shared-tree any output under usr/lib/node_modules
runtime-npm a runtime dep on a whole Node runtime, or a subset that includes npm/npx/node_modules
flavor build or runtime deps on node instead of node-lts. This is Karl's convention from #752; CURRENT_ALLOWED is the documented escape hatch, with a reason per entry.

Collections (stacks) are exempt from runtime-npm and flavor, because a stack naming a Node runtime is the user choosing one. It fails closed: a dump or parse failure exits 2, and the check goes red instead of passing unverified. Unlike the report-only sibling workflows, this one is meant to block. Whether to make it a branch-protection required check is your call.

Full-catalog dry run (both arches):

⚠ This check is red until #752 merges. Merge #752 first, then I'll update this branch.

3. Documented in AGENTS.md

AGENTS.md gets a new Node pattern section beside the Go and Rust patterns. It covers the three rules and gives a worked build.sh and build.ncl, so authors and agents learn the convention before the guard has to reject anything. pkgmgr import npm generates the same layout (gominimal/pkgmgr-rs#873, merged).

Verification

It hasn't been built locally (Linux sandbox builds only). CI builds the five changed packages and runs their standalone tests. Those tests exercise the new bin symlinks (cf --version, wrangler --version, capy --version, mmdc --help, next --version, plus next's sharp/libvips requires).

🤖 Generated with Claude Code

#751)

Two follow-ups to #752:

1. cf, wrangler, next, mermaid-cli and capy installed into the shared
   usr/lib/node_modules, the tree the node runtime's own npm lives in
   (Tom's #370 warning). They now install into usr/libexec/<pkg> with
   relative usr/bin symlinks, like vlt and pnpm. A `test -e` on each
   link fails the build if an upstream bin is renamed, rather than
   shipping a dangling link. mermaid-cli's wrapper and next's
   sharp/test paths follow the move.

2. node-runtime-guard (BLOCKING, as Karl suggested in #751). It
   evaluates the whole catalog on amd64 and arm64 and rejects:
   - outputs under usr/lib/node_modules;
   - runtime deps that carry a node runtime's npm;
   - packages on `node` rather than `node-lts`.
   A dump failure exits 2 and fails the check, so an unverified run
   cannot go green.

Full-catalog dry run:
- main: FAILS 20 packages (34 flavor, 19 runtime-npm, 5 shared-tree).
- main + #752 + this: PASSES all 531 packages.
This check is red until #752 lands.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@bryan-minimal
bryan-minimal requested a review from a team as a code owner September 23, 2026 18:19
@coderabbitai

coderabbitai Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Ask an admin to make reviews automatic

Open in CodeRabbit

Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing.

Next included review available in 22 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 2 included reviews currently available. Your 56 included PR review attempts over the past 7 days set your current allowance at 2 reviews per hour.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository: gominimal/pkgs/.coderabbit.yaml

Review profile: CHILL

Plan: Essentials

Run ID: f2598b60-fb1f-41b3-93c0-e1060424f510

📥 Commits

Reviewing files that changed from the base of the PR and between 57dd648 and 9df85ff.

📒 Files selected for processing (13)
  • .github/scripts/node_runtime_guard.py
  • .github/workflows/node-runtime-guard.yml
  • AGENTS.md
  • packages/capy/build.ncl
  • packages/capy/build.sh
  • packages/cf/build.ncl
  • packages/cf/build.sh
  • packages/mermaid-cli/build.ncl
  • packages/mermaid-cli/build.sh
  • packages/next/build.ncl
  • packages/next/build.sh
  • packages/wrangler/build.ncl
  • packages/wrangler/build.sh

Comment @coderabbitai help to get the list of available commands.

The rules that node-runtime-guard enforces are now written down where
package authors and agents look first:
- node-lts, not node;
- interpreter-only at runtime;
- a private libexec prefix, with a worked build.sh and build.ncl.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@mitodrummer mitodrummer 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 looks good and I'm happy to approve after the two fixes below, merging after #752.

Checked:

  • Every npm install -g in the repo at this head now uses a private usr/libexec/<pkg> prefix, so no package was missed.
  • The test -e and cli.js checks fail the build instead of shipping a dangling link. The next and mermaid-cli tests point at the new paths.
  • gominimal/webapp doesn't reference lib/node_modules anywhere, so moving cf is safe for us.
  • #752 now uses subsetOf node-lts ["node"] at runtime and node-lts for builds, which matches what the flavor rule enforces.
  • The guard's current failure ("20 package(s) FAIL (of 531)") is the expected pre-#752 result.

Should fix

  1. shared-tree misses broad globs. It only flags a glob equal to or under usr/lib/node_modules, but usr/** and usr/lib/** capture that tree too. The catalog already has 11 of these (gnupg, lsof, stow, postgres, gcc, libgcrypt, …). None are Node packages today, but a future npm-installing package with everything = { glob = "usr/**" } would ship an npm tree and pass the guard. Suggest also flagging any glob whose literal prefix (the part before the first wildcard) is an ancestor of usr/lib/node_modules. To avoid failing the existing C libraries, limit that to packages whose build_deps include a node runtime.
  2. AGENTS.md says more than the guard checks. The Node pattern section says node-lts applies to build_deps, runtime_deps and test_deps. check() only reads build_deps and runtime_deps. No package uses node in a test dep today as far as I can see. Either extend the flavor rule to test deps, or drop test_deps from the doc so the two agree.

Consider

  • In capy/build.ncl the output is still called node_modules, and its comment still says "Same shape as cf's node_modules". Both are stale now; the other four packages call it libexec.
  • On required-check status: yes, let's make node-runtime-guard a required check once #752 is in and it's green. It only protects anything if it's enforced.

…ame (review)

Addresses Karl's review on #753:

- shared-tree now also rejects a broad recursive glob (`usr/**`,
  `usr/lib/**`, `**`) whose literal prefix is an ancestor of
  usr/lib/node_modules, in any package that BUILDS with a node runtime.
  Such a glob would capture an npm install and ship it. Non-node packages
  are exempt, so the 11 C libraries using `usr/**` are untouched.
  - Synthetic cases: usr/**, usr/lib/** and ** are flagged; usr/bin/*,
    usr/lib/*.so, usr/share/** and a C lib's usr/** are not.
  - Full catalog: main still fails the same 20 packages; with #752 it
    passes all 531.
- AGENTS.md now claims only what the guard checks: build and runtime
  deps. node-lts for test_deps is stated as convention, since `minimal
  dump` has no tests to check.
- capy: rename the output node_modules -> libexec to match the other
  four, and drop the stale "same shape as cf's node_modules" comment.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@bryan-minimal

Copy link
Copy Markdown
Member Author

Thanks! Both should-fixes are in 9df85ff.

  • Broad globs: shared-tree now also flags a recursive glob whose literal prefix is an ancestor of usr/lib/node_modules (usr/**, usr/lib/**, **), only for packages that build with a node runtime.
  • AGENTS.md now claims only what the guard checks (build and runtime deps). minimal dump has no tests, so node-lts for test_deps is stated as convention.
  • capy: output renamed to libexec and the stale comment dropped. Nothing referenced the old name.

Agreed on making the guard a required check once #752 is in and green.

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

Approving. Both should-fix items are addressed in 9df85ff, and the broad-glob rule flags nothing on today's catalog. The guard failure is the expected set (the same 20 packages #752 changes), so merge after #752 and a rebase, once the guard is green.

@bryan-minimal
bryan-minimal added this pull request to the merge queue Sep 23, 2026
Merged via the queue into main with commit 6f8cd43 Sep 23, 2026
12 of 13 checks passed
@bryan-minimal
bryan-minimal deleted the node-private-libexec branch September 23, 2026 19:07
bryan-minimal added a commit that referenced this pull request Sep 23, 2026
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