Skip to content

feat(compile): zero-config bindings — auto-compile default + faithfulness marker - #7137

Open
jdalton wants to merge 1 commit into
PerryTS:mainfrom
jdalton:feat/zero-config-bindings
Open

feat(compile): zero-config bindings — auto-compile default + faithfulness marker#7137
jdalton wants to merge 1 commit into
PerryTS:mainfrom
jdalton:feat/zero-config-bindings

Conversation

@jdalton

@jdalton jdalton commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

What & why

Moves perry toward "it just works" for dependents (Socket Firewall is the driving case) — no hand-maintained perry.compilePackages list and no node_modules offloading needed.

Landed

  1. Auto-compile is the default. A project that does not pin a perry.compilePackages value gets its whole reachable node_modules graph compiled automatically (host_config injects the universal "*" and auto-satisfies the allow.compilePackages two-key check). Perry is a compiler, not a supply-chain gate — "this package is not on a list" is no longer a hard error; a dependency that genuinely can't compile surfaces as an ordinary compile error. Native-shimmed packages still resolve to their bundled bindings (the Compile a standard Express app: full blocker inventory (codegen emits undefined @perry_closure_* globals → linked-but-crashing binary) #3527 wildcard expansion skips them). Opt out with an explicit array, or compilePackages: false / [] to compile nothing and re-arm the V8-free gate.

  2. compilePackages: "auto" / allow.compilePackages: true — explicit spelling of that default (sugar for ["*"]).

  3. Faithfulness marker. Well-known bindings gain compat = "full" | "partial" (absent ⇒ partial, conservative). Auto-preferring a partial binding over an installed node_modules copy now prints a one-line transparency note; PERRY_REQUIRE_FAITHFUL_BINDINGS=1 turns that into a refusal. Default off ⇒ existing builds byte-identical. Audited: dotenv/nanoid/slugify/uuidfull; undici/node-forge/lru-cachepartial (documented subsets).

Design note + rationale (incl. the resolved auto-compile-default policy and the firewall cleanup this unblocks): docs/src/native-libraries/zero-config-and-faithfulness.md.

Context / finding

undici/node-forge/iovalkey/dotenv were added to NATIVE_MODULES in #7032/#7033, so on current main all four already resolve to their bindings even with a node_modules copy present — the "binding gets shadowed" problem is already gone. This PR adds the safety/transparency layer around that (the compat marker) and the auto-compile default that lets a dependent drop its compilePackages list entirely.

Verification

  • 843/843 perry bin unit tests pass; new well_known::compat_* + audit tests added.
  • Manual repros: zero-config build compiles a non-binding dep and runs; all four firewall packages resolve to bindings with no config and no offload; partial note fires (and PERRY_REQUIRE_FAITHFUL_BINDINGS=1 refuses) for undici/node-forge/iovalkey; full dotenv stays silent; compilePackages: [] restores the gate.

Notes

Summary by CodeRabbit

  • New Features

    • Package compilation now automatically includes reachable dependencies by default.
    • Added compilePackages options: true, "auto", "all", arrays, or explicit opt-outs.
    • Added compatibility markers distinguishing fully faithful and partially supported native bindings.
    • Added strict mode with PERRY_REQUIRE_FAITHFUL_BINDINGS=1 to reject partial bindings.
  • Transparency

    • Compilation output now reports when a bundled partial binding takes precedence over an installed package.
  • Documentation

    • Added guidance on zero-configuration native bindings, compatibility markers, precedence, and configuration options.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 35a59801-11bb-4174-b90b-d114ca9658bf

📥 Commits

Reviewing files that changed from the base of the PR and between 66e3e92 and 1f285c8.

📒 Files selected for processing (8)
  • changelog.d/zero-config-binding-faithfulness.md
  • crates/perry/src/commands/compile/collect_modules.rs
  • crates/perry/src/commands/compile/host_config.rs
  • crates/perry/src/commands/compile/run_pipeline.rs
  • crates/perry/src/commands/compile/types.rs
  • crates/perry/src/commands/compile/well_known.rs
  • crates/perry/well_known_bindings.toml
  • docs/src/native-libraries/zero-config-and-faithfulness.md
🚧 Files skipped from review as they are similar to previous changes (6)
  • crates/perry/src/commands/compile/collect_modules.rs
  • crates/perry/src/commands/compile/host_config.rs
  • crates/perry/src/commands/compile/run_pipeline.rs
  • crates/perry/src/commands/compile/types.rs
  • crates/perry/src/commands/compile/well_known.rs
  • crates/perry/well_known_bindings.toml

📝 Walkthrough

Walkthrough

The compiler now classifies well-known bindings as full or partial, compiles reachable packages by default, and detects installed packages shadowed by partial bundled bindings. Text builds report these selections, while strict mode rejects them.

Changes

Binding faithfulness and zero-config compilation

Layer / File(s) Summary
Binding compatibility contract
crates/perry/src/commands/compile/well_known.rs, crates/perry/well_known_bindings.toml
Bindings default to Partial, parse explicit compatibility metadata, and expose is_faithful(). Shipped bindings declare full or partial support.
Universal package compilation routing
crates/perry/src/commands/compile/host_config.rs
compilePackages accepts true, "auto", and "all". Unconfigured projects compile reachable packages by default. Explicit empty or disabling values suppress this behavior.
Partial binding precedence and enforcement
crates/perry/src/commands/compile/collect_modules.rs, crates/perry/src/commands/compile/types.rs, crates/perry/src/commands/compile/run_pipeline.rs
The compiler detects installed package copies, records partial bundled-binding selections, reports them in text mode, and rejects them when PERRY_REQUIRE_FAITHFUL_BINDINGS=1 is set.
Faithfulness policy documentation
docs/src/native-libraries/zero-config-and-faithfulness.md, changelog.d/zero-config-binding-faithfulness.md
Documentation describes binding precedence, compatibility markers, strict enforcement, automatic compilation, and opt-outs.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CompilePipeline
  participant ModuleCollector
  participant NodeModules
  participant WellKnownBinding
  participant CompilationContext
  CompilePipeline->>ModuleCollector: Collect native imports
  ModuleCollector->>NodeModules: Search ancestor node_modules copies
  NodeModules-->>ModuleCollector: Installed package status
  ModuleCollector->>WellKnownBinding: Check is_faithful()
  WellKnownBinding-->>ModuleCollector: Full or Partial
  ModuleCollector->>CompilationContext: Record partial binding selection
  CompilePipeline->>CompilationContext: Read recorded selections
  CompilePipeline-->>CompilePipeline: Report note or enforce strict mode
Loading

Possibly related PRs

  • PerryTS/perry#7033: This PR updates faithfulness and precedence handling for the node-forge binding introduced by that change.
  • PerryTS/perry#7118: Both changes modify compile-time handling of well-known native bindings, but this PR adds faithfulness metadata and automatic package compilation.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the PR's primary changes: zero-config compilation and binding faithfulness markers.
Description check ✅ Passed The description is detailed and covers the summary, changes, rationale, verification, and documentation updates, but it omits several template headings and checklist items.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Moves perry toward "it just works" for dependents (Socket Firewall is the
driving case) — no hand-maintained perry.compilePackages list and no
node_modules offloading needed.

Auto-compile default: a project that does not pin perry.compilePackages now
gets its whole reachable node_modules graph compiled automatically (host_config
injects the universal "*" and auto-satisfies the allow.compilePackages two-key
check). Perry is a compiler, not a supply-chain gate — "this package is not on
a list" is no longer a hard error; a dependency that genuinely cannot compile
surfaces as an ordinary compile error. Native-shimmed packages still resolve to
their bundled bindings (the PerryTS#3527 wildcard expansion skips them). Opt out with
an explicit array, or compilePackages: false / [] to compile nothing and
re-arm the V8-free gate. perry.compilePackages: "auto" (and
allow.compilePackages: true) are the explicit spelling of the default.

Faithfulness marker: well-known bindings gain compat = "full" | "partial"
(absent => partial, conservative) in well_known_bindings.toml, parsed into
BindingCompat with an is_faithful() helper. When perry auto-prefers a partial
binding over an installed node_modules copy it prints a one-line transparency
note; PERRY_REQUIRE_FAITHFUL_BINDINGS=1 turns that into a refusal (default off,
so existing builds are byte-identical).

Faithfulness audit (conservative — default to partial unless clearly complete):
dotenv / nanoid / slugify / uuid => full (small, complete pure ports);
undici (dispatcher-only), node-forge (PKI-only), lru-cache => explicit partial
with rationale; every other binding inherits the partial default.

Design note + firewall-cleanup guidance:
docs/src/native-libraries/zero-config-and-faithfulness.md
@jdalton
jdalton force-pushed the feat/zero-config-bindings branch from 66e3e92 to 1f285c8 Compare July 31, 2026 17:27

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@changelog.d/zero-config-binding-faithfulness.md`:
- Around line 27-52: Remove the duplicate ### Added heading in the changelog by
moving the perry.compilePackages: "auto" bullet into the existing first ###
Added section, keeping all Added entries grouped before ### Changed.
- Line 1: Rename the changelog fragment from zero-config-binding-faithfulness.md
to the required <PR_NUMBER>-zero-config-binding-faithfulness.md format,
replacing <PR_NUMBER> with the actual pull request number and preserving the
existing contents.

In `@crates/perry/src/commands/compile/collect_modules.rs`:
- Around line 1224-1242: Add PERRY_REQUIRE_FAITHFUL_BINDINGS to the
BUILD_CACHE_ENV_VARS list in build_cache.rs so cache keys include the
strict-binding mode. Preserve the existing collect_modules refusal behavior and
include the variable alongside the other PERRY_* build-affecting environment
settings.
- Around line 1214-1223: Update the well-known binding lookup in the bare-import
handling around parse_package_specifier to try the full import.source specifier
first, then fall back to the parsed bare package name. Ensure the resulting
binding is used for the existing node_builtin, is_faithful, transparency-note,
and PERRY_REQUIRE_FAITHFUL_BINDINGS logic, including subpath keys such as
mysql2/promise.

In `@crates/perry/src/commands/compile/host_config.rs`:
- Around line 682-706: Track whether PERRY_ALLOW_PERRY_FEATURES=0 explicitly
activated the fail-closed override while processing the existing environment
handling. Update the universal-allow injection after the auto-compile default so
it does not add "*" to ctx.allow_compile_packages when that override is active,
while preserving current wildcard behavior otherwise.

In `@crates/perry/src/commands/compile/well_known.rs`:
- Around line 88-102: Update WellKnownBinding::is_faithful() to resolve alias_of
and inherit the target binding’s compat status, so aliases such as
mysql2/promise and fetch are faithful when their targets are Full. Preserve the
current conservative behavior for non-alias rows and unresolved or non-Full
targets, using the existing well-known binding lookup mechanism.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5421f9ea-c034-40aa-a6a4-959619429e18

📥 Commits

Reviewing files that changed from the base of the PR and between a3b31c0 and 66e3e92.

📒 Files selected for processing (8)
  • changelog.d/zero-config-binding-faithfulness.md
  • crates/perry/src/commands/compile/collect_modules.rs
  • crates/perry/src/commands/compile/host_config.rs
  • crates/perry/src/commands/compile/run_pipeline.rs
  • crates/perry/src/commands/compile/types.rs
  • crates/perry/src/commands/compile/well_known.rs
  • crates/perry/well_known_bindings.toml
  • docs/src/native-libraries/zero-config-and-faithfulness.md

@@ -0,0 +1,52 @@
### Added

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Rename the fragment to the required <PR>-<slug>.md format.

This file is changelog.d/zero-config-binding-faithfulness.md, missing the PR-number prefix. As per coding guidelines, changelog.d/*.md fragments must follow changelog.d/<PR>-<slug>.md: "Add a PR-keyed changelog.d/<PR>-<slug>.md fragment without a version header for every change; never append to CHANGELOG.md." Based on learnings, the changeset-gate CI job enforces the <PR>-<slug>.md naming, so this filename would likely fail that check.

Rename the file, e.g. changelog.d/<PR_NUMBER>-zero-config-binding-faithfulness.md.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@changelog.d/zero-config-binding-faithfulness.md` at line 1, Rename the
changelog fragment from zero-config-binding-faithfulness.md to the required
<PR_NUMBER>-zero-config-binding-faithfulness.md format, replacing <PR_NUMBER>
with the actual pull request number and preserving the existing contents.

Sources: Coding guidelines, Learnings

Comment on lines +27 to +52
### Changed

- **Auto-compile is now the default — no `perry.compilePackages` allowlist
required.** A project that does not pin a `perry.compilePackages` value gets
its whole reachable `node_modules` dependency graph compiled automatically.
Perry is a compiler, not a supply-chain gate: faithfully compiling code that
is already installed is not perry's decision to block, and "this package is
not on a list" is no longer a hard error. A dependency that genuinely cannot
be compiled surfaces as an ordinary **compile error** with a diagnostic, not
a policy refusal. Native-shimmed packages still resolve to their bundled
bindings (the wildcard expansion skips them), so bindings keep winning.

Supply-chain hygiene (pinning, lockfiles, review, dedicated tooling) is the
user's responsibility. Opt out to the old "listed packages only" behavior
with an explicit `perry.compilePackages` array, or `compilePackages: false` /
`[]` to compile nothing and re-arm the V8-free gate.

### Added

- **`perry.compilePackages: "auto"` (and `perry.allow.compilePackages: true`).**
Explicit spelling of the new default — sugar for the universal `["*"]`
wildcard (#3527), useful to document intent or to re-enable auto-compile
inside a config that would otherwise constrain it. `"auto"` / `"all"` /
`true` are accepted; an explicit array is unchanged. A universal-trust value
auto-satisfies `perry.allow.compilePackages`, and native-shimmed packages
stay on their bindings exactly as with a literal `"*"`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Merge the duplicate ### Added heading.

The file has ### Added (line 1), ### Changed (line 27), then a second ### Added (line 44). Static analysis (markdownlint) flags this as "Multiple headings with the same content." Move the perry.compilePackages: "auto" bullet into the first ### Added section, or reorder so all ### Added bullets are grouped together, ahead of ### Changed.

🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 44-44: Multiple headings with the same content

(MD024, no-duplicate-heading)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@changelog.d/zero-config-binding-faithfulness.md` around lines 27 - 52, Remove
the duplicate ### Added heading in the changelog by moving the
perry.compilePackages: "auto" bullet into the existing first ### Added section,
keeping all Added entries grouped before ### Changed.

Source: Linters/SAST tools

Comment on lines +1214 to +1223
let is_bare = !import.source.starts_with('.') && !import.source.starts_with('/');
if is_bare {
let (pkg_name, _) = parse_package_specifier(&import.source);
if let Some(binding) = super::well_known::lookup_well_known(&pkg_name) {
// Node builtins (net/http/zlib/events/…) are always native
// and have no meaningful npm copy to shadow — skip them.
if !binding.node_builtin
&& !binding.is_faithful()
&& node_modules_copy_on_disk(&pkg_name, entry_path, &canonical)
{

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n '^\[bindings\.[^]]*/[^]]*\]' crates/perry/well_known_bindings.toml -A6

Repository: PerryTS/perry

Length of output: 930


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== target lines =="
sed -n '1190,1245p' crates/perry/src/commands/compile/collect_modules.rs

echo
echo "== well_known relevant =="
sed -n '1,90p' crates/perry/src/commands/compile/well_known.rs

echo
echo "== parse_package_specifier usages/definition =="
rg -n "parse_package_specifier|struct WellKnownBinding|impl WellKnownBinding|pub fn lookup_well_known" crates/perry/src/commands/compile/well_known.rs crates/perry/src/commands/compile -g '*.rs'

echo
echo "== TOML parsed table names with slashes =="
python3 - <<'PY'
import tomllib
from pathlib import Path
data = tomllib.loads(Path('crates/perry/well_known_bindings.toml').read_text())
for k,v in data.items():
    if k.startswith('bindings.') and '/' in k:
        print(f"{k}\tother={v.get('alias-of')} partial={v.get('partial')} node_builtin={v.get('node_builtin')}")
PY

Repository: PerryTS/perry

Length of output: 9854


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== parse_package_specifier implementation =="
sed -n '400,460p' crates/perry/src/commands/compile/resolve.rs

echo
echo "== well_known implementation =="
sed -n '130,190p' crates/perry/src/commands/compile/well_known.rs

echo
echo "== bindings sections with slashes from source =="
sed -n '420,445p' crates/perry/well_known_bindings.toml
sed -n '545,575p' crates/perry/well_known_bindings.toml

echo
echo "== parse specifiers =="
python3 - <<'PY'
from pathlib import Path
text = Path('crates/perry/src/commands/compile/resolve.rs').read_text()
start = text.index('pub(super) fn parse_package_specifier')
end = text.find('\n', text.index('{', start) + len('{')) + 1
body = text[start:end]
print(body[:1200])
PY

Repository: PerryTS/perry

Length of output: 7607


Also match well-known bindings by full subpath specifier before applying bare-package rules.

Registered well-known bindings can use subpath keys like "mysql2/promise", but this block only calls lookup_well_known() with the package name returned by parse_package_specifier, and parse_package_specifier("mysql2/promise") returns the bare name "mysql2". A subpath-keyed partial binding will therefore skip the transparency note and the PERRY_REQUIRE_FAITHFUL_BINDINGS refusal when an installed node_modules copy exists.

Lookup the full import source first, then fall back to the bare package name.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry/src/commands/compile/collect_modules.rs` around lines 1214 -
1223, Update the well-known binding lookup in the bare-import handling around
parse_package_specifier to try the full import.source specifier first, then fall
back to the parsed bare package name. Ensure the resulting binding is used for
the existing node_builtin, is_faithful, transparency-note, and
PERRY_REQUIRE_FAITHFUL_BINDINGS logic, including subpath keys such as
mysql2/promise.

Comment on lines +1224 to +1242
if std::env::var_os("PERRY_REQUIRE_FAITHFUL_BINDINGS").is_some() {
anyhow::bail!(
"`{pkg}` resolves to the bundled native binding \
`{krate}`, which is a PARTIAL drop-in (not the full \
npm API), but a `node_modules/{pkg}` copy is installed \
and `PERRY_REQUIRE_FAITHFUL_BINDINGS` is set. Refusing \
to auto-prefer the partial binding.\n\
\n\
Either add `{pkg}` to `perry.compilePackages` (+ \
`perry.allow.compilePackages`) to AOT-compile the real \
JavaScript, or unset PERRY_REQUIRE_FAITHFUL_BINDINGS to \
accept the partial binding.\n\
\n\
(imported from {importer})",
pkg = pkg_name,
krate = binding.krate,
importer = canonical.display(),
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect BuildCacheProbe's fingerprint inputs for PERRY_REQUIRE_FAITHFUL_BINDINGS coverage.
fd -a build_cache.rs crates/perry/src/commands/compile | xargs -I{} rg -n -C6 'fn probe\(|fingerprint|PERRY_REQUIRE_FAITHFUL_BINDINGS|env::var' {}

Repository: PerryTS/perry

Length of output: 8941


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
fd -a . crates/perry/src/commands/compile | sed -n '1,120p'

echo "== BUILD_CACHE_ENV_VARS current_perry_fingerprint current_env references =="
rg -n -C4 'BUILD_CACHE_ENV_VARS|current_env|current_perry_fingerprint|PERRY_REQUIRE_FAITHFUL_BINDINGS|PERRY_DEBUG_SYMBOLS|PERRY_TARGET_CPU|apply_pkg_and_toml_config|run_pipeline' crates/perry/src/commands/compile

echo "== run_pipeline relevant area =="
fd -a run_pipeline.rs crates/perry/src/commands/compile | xargs -I{} sed -n '1,260p' {}

echo "== build_cache relevant area =="
fd -a build_cache.rs crates/perry/src/commands/compile | xargs -I{} sed -n '450,560p' {}

Repository: PerryTS/perry

Length of output: 50369


Add PERRY_REQUIRE_FAITHFUL_BINDINGS to BUILD_CACHE_ENV_VARS.

build_cache.rs includes PERRY_DEBUG_SYMBOLS and PERRY_TARGET_CPU in the build cache env list, but not PERRY_REQUIRE_FAITHFUL_BINDINGS, while the strict partial-binding refusal reads that variable only during collect_modules. A cache hit can therefore return a build from a non-strict run instead of running the refusal path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry/src/commands/compile/collect_modules.rs` around lines 1224 -
1242, Add PERRY_REQUIRE_FAITHFUL_BINDINGS to the BUILD_CACHE_ENV_VARS list in
build_cache.rs so cache keys include the strict-binding mode. Preserve the
existing collect_modules refusal behavior and include the variable alongside the
other PERRY_* build-affecting environment settings.

Comment on lines +682 to +706
// Auto-compile default (owner policy): a project that does not pin a
// `perry.compilePackages` value gets its whole reachable node_modules
// graph compiled — perry is a compiler, not a supply-chain gate, so
// "unlisted" is not a hard error, it just compiles (and a dependency that
// genuinely can't compile surfaces as an ordinary compile error). This is
// exactly `compilePackages: "auto"`: inject the universal `"*"`, which the
// expansion just below turns into concrete installed names (native-shimmed
// packages are skipped, so bundled bindings still win). Opt out with an
// explicit list, or `compilePackages: false` / `[]` to compile nothing and
// restore the V8-free gate's "listed only" behavior.
if !compile_packages_explicit {
ctx.compile_packages.insert("*".to_string());
}
// Universal trust ⇒ universal allow. Whenever routing includes the `"*"`
// wildcard (from this auto default, the `"auto"` scalar, or a literal
// `["*"]`), the host is trusting its whole graph, so the #497 two-key
// allowlist below must not then block it. Supply-chain review is the
// user's responsibility (lockfiles, pinning, external tooling), not a
// hand-maintained perry allowlist.
if ctx.compile_packages.iter().any(|p| p == "*")
&& !ctx.allow_compile_packages.iter().any(|p| p == "*")
{
ctx.allow_compile_packages.push("*".to_string());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

PERRY_ALLOW_PERRY_FEATURES=0 no longer enforces its fail-closed guarantee for the new auto-compile default.

The pre-existing PERRY_ALLOW_PERRY_FEATURES=0 branch clears ctx.allow_compile_packages and is documented as a "fail-closed CI gate" that "enforces the refusal even when package.json opted in." This new "Universal trust ⇒ universal allow" block runs after that env-var handling and unconditionally re-adds "*" to ctx.allow_compile_packages whenever ctx.compile_packages contains "*".

Because the auto-compile default now inserts "*" into ctx.compile_packages for any project that does not set an explicit perry.compilePackages value (the common case), setting PERRY_ALLOW_PERRY_FEATURES=0 no longer prevents the allowlist from being auto-satisfied. The same applies to an explicit compilePackages: "auto" / true / ["*"]. A CI job relying on this env var to force a hard refusal on compiling arbitrary node_modules code into the binary will silently get a passing, fully-compiled build instead.

Track whether the fail-closed override was explicitly requested and skip the universal-allow injection in that case.

🔒 Proposed fix to respect the fail-closed override
+    let mut compile_packages_force_denied = false;
     match std::env::var("PERRY_ALLOW_PERRY_FEATURES") {
         Ok(v) if v == "1" || v.eq_ignore_ascii_case("true") => {
             if !ctx.allow_native_library.iter().any(|s| s == "*") {
                 ctx.allow_native_library.push("*".to_string());
             }
             if !ctx.allow_compile_packages.iter().any(|s| s == "*") {
                 ctx.allow_compile_packages.push("*".to_string());
             }
         }
         Ok(v) if v == "0" || v.eq_ignore_ascii_case("false") => {
             ctx.allow_native_library.clear();
             ctx.allow_compile_packages.clear();
+            compile_packages_force_denied = true;
         }
         _ => {}
     }
     ...
     if ctx.compile_packages.iter().any(|p| p == "*")
         && !ctx.allow_compile_packages.iter().any(|p| p == "*")
+        && !compile_packages_force_denied
     {
         ctx.allow_compile_packages.push("*".to_string());
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Auto-compile default (owner policy): a project that does not pin a
// `perry.compilePackages` value gets its whole reachable node_modules
// graph compiled — perry is a compiler, not a supply-chain gate, so
// "unlisted" is not a hard error, it just compiles (and a dependency that
// genuinely can't compile surfaces as an ordinary compile error). This is
// exactly `compilePackages: "auto"`: inject the universal `"*"`, which the
// expansion just below turns into concrete installed names (native-shimmed
// packages are skipped, so bundled bindings still win). Opt out with an
// explicit list, or `compilePackages: false` / `[]` to compile nothing and
// restore the V8-free gate's "listed only" behavior.
if !compile_packages_explicit {
ctx.compile_packages.insert("*".to_string());
}
// Universal trust ⇒ universal allow. Whenever routing includes the `"*"`
// wildcard (from this auto default, the `"auto"` scalar, or a literal
// `["*"]`), the host is trusting its whole graph, so the #497 two-key
// allowlist below must not then block it. Supply-chain review is the
// user's responsibility (lockfiles, pinning, external tooling), not a
// hand-maintained perry allowlist.
if ctx.compile_packages.iter().any(|p| p == "*")
&& !ctx.allow_compile_packages.iter().any(|p| p == "*")
{
ctx.allow_compile_packages.push("*".to_string());
}
// Auto-compile default (owner policy): a project that does not pin a
// `perry.compilePackages` value gets its whole reachable node_modules
// graph compiled — perry is a compiler, not a supply-chain gate, so
// "unlisted" is not a hard error, it just compiles (and a dependency that
// genuinely can't compile surfaces as an ordinary compile error). This is
// exactly `compilePackages: "auto"`: inject the universal `"*"`, which the
// expansion just below turns into concrete installed names (native-shimmed
// packages are skipped, so bundled bindings still win). Opt out with an
// explicit list, or `compilePackages: false` / `[]` to compile nothing and
// restore the V8-free gate's "listed only" behavior.
if !compile_packages_explicit {
ctx.compile_packages.insert("*".to_string());
}
// Universal trust ⇒ universal allow. Whenever routing includes the `"*"`
// wildcard (from this auto default, the `"auto"` scalar, or a literal
// `["*"]`), the host is trusting its whole graph, so the `#497` two-key
// allowlist below must not then block it. Supply-chain review is the
// user's responsibility (lockfiles, pinning, external tooling), not a
// hand-maintained perry allowlist.
if ctx.compile_packages.iter().any(|p| p == "*")
&& !ctx.allow_compile_packages.iter().any(|p| p == "*")
&& !compile_packages_force_denied
{
ctx.allow_compile_packages.push("*".to_string());
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry/src/commands/compile/host_config.rs` around lines 682 - 706,
Track whether PERRY_ALLOW_PERRY_FEATURES=0 explicitly activated the fail-closed
override while processing the existing environment handling. Update the
universal-allow injection after the auto-compile default so it does not add "*"
to ctx.allow_compile_packages when that override is active, while preserving
current wildcard behavior otherwise.

Comment on lines +88 to +102
/// How faithful this wrapper is to the npm package's public API.
/// Governs whether auto-preference over an on-disk `node_modules`
/// copy is safe-by-construction. Absent in the toml → `Partial`.
pub compat: BindingCompat,
}

impl WellKnownBinding {
/// Whether this binding is an audited complete drop-in — safe to
/// auto-prefer over a user's `node_modules` copy without a caveat.
pub fn is_faithful(&self) -> bool {
// Aliases inherit the faithfulness of their target (resolved by
// the caller when needed); the row's own `compat` still applies
// as a conservative floor.
matches!(self.compat, BindingCompat::Full)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check alias entries and whether they set their own `compat`.
rg -n -B3 -A10 'alias-of' crates/perry/well_known_bindings.toml

Repository: PerryTS/perry

Length of output: 1135


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== well_known.rs relevant section =="
sed -n '1,180p' crates/perry/src/commands/compile/well_known.rs

echo
echo "== well_known_bindings entries around mysql2/fetch aliases =="
sed -n '420,490p' crates/perry/well_known_bindings.toml

echo
echo "== collect_modules relevant section =="
sed -n '1180,1245p' crates/perry/src/commands/compile/collect_modules.rs

echo
echo "== all alias-of occurrences in well_known_bindings =="
python3 - <<'PY'
from pathlib import Path
p=Path('crates/perry/well_known_bindings.toml')
text=p.read_text()
lines=text.splitlines()
cur=None
compat_vals=[]
for i,l in enumerate(lines,1):
    if l.startswith('[') and l.endswith(']'):
        cur=l.strip('[]')
    if 'alias-of' in l:
        print(f"alias row {i}: {cur}")
        # print up/down context until next table or 6 lines
        start=max(1,i-4); end=min(len(lines),i+8)
        for n in range(start,end+1):
            print(f"{n}: {lines[n-1]}")
        print("---")
print("binding compat defaults/present scan:")
# simple structural scan
PY
rg -n '^\[bindings\.' crates/perry/well_known_bindings.toml | sed -n 'x;p' | rg -n -B0 -A0 'fetch\]|mysql2\]'
rg -n 'alias-of|compat|name:|crate:|crate =|lib =|upstream|compat =' crates/perry/well_known_bindings.toml | sed -n '/fetch\]/,+15p;/mysql2\]/,+15p'
sed -n '/^\[bindings\.fetch\]/,/^\[bindings\.ws\]/p' crates/perry/well_known_bindings.toml | head -n 80
sed -n '/^\[bindings\.mysql2-upstream-mysql\]/,/^\[bindings\.nodemailer\]/p' crates/perry/well_known_bindings.toml | head -n 80

Repository: PerryTS/perry

Length of output: 15552


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== BindingCompat occurrences in crates/perry/src =="
rg -n 'BindingCompat|compat|faithful|perry\.compilePackages|PERRY_REQUIRE_FAITHFUL_BINDINGS|=full|="full"|- full|full|partial' crates/perry/src
rg -n 'BindingCompat|compat|is_faithful|alias_of|well_known_bindings' crates/perry/src/crates/perry -g '*.rs' 2>/dev/null || true

echo
echo "== manifest/docs references to compat/faithful =="
rg -n -i 'compat|faithful|partial drop|just works|PERRY_REQUIRE_FAITHFUL_BINDINGS' crates/perry docs 2>/dev/null || true

Repository: PerryTS/perry

Length of output: 50370


Inherit compat through alias_of in is_faithful().

mysql2/promise and fetch are alias rows without compat, so they default to Partial even if their targets are Full. Since collect_modules.rs only checks binding.is_faithful() directly, resolve alias_of as part of faithfulness so aliases are not incorrectly marked partial.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry/src/commands/compile/well_known.rs` around lines 88 - 102,
Update WellKnownBinding::is_faithful() to resolve alias_of and inherit the
target binding’s compat status, so aliases such as mysql2/promise and fetch are
faithful when their targets are Full. Preserve the current conservative behavior
for non-alias rows and unresolved or non-Full targets, using the existing
well-known binding lookup mechanism.

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